Объектно-ориентированное программирование на Python: принципы и практика

Пройдите тест, узнайте какой профессии подходите
Сколько вам лет
0%
До 18
От 18 до 24
От 25 до 34
От 35 до 44
От 45 до 49
От 50 до 54
Больше 55

Для кого эта статья:

  • Студенты и новички в программировании, интересующиеся Python и ООП
  • Опытные разработчики, желающие углубить знания в объектно-ориентированном программировании
  • Преподаватели и тренеры, обучающие Python и концепциям ООП

    Объектно-ориентированное программирование на Python — не просто модный термин, а мощный инструмент структурирования кода, который трансформирует хаотичные строки в элегантную архитектуру. Когда я впервые столкнулся с ООП, оно казалось непостижимой магией — но стоило разобраться в базовых концепциях, как сложные проекты стали распадаться на понятные компоненты, словно конструктор Lego. Готовы превратить запутанные скрипты в организованные системы? Давайте разберемся в основах ООП на Python, без лишней теории и с практическими примерами. 🐍

Хотите не просто понять, а мастерски применять ООП в реальных проектах? Программа Обучение Python-разработке от Skypro погружает вас в мир объектно-ориентированного программирования через практику. Вы создадите полноценные веб-приложения с использованием классов и объектов под руководством экспертов, работающих в крупнейших IT-компаниях. Реальные проекты в портфолио вместо сухой теории — ваш путь к позиции Python-разработчика за 9 месяцев.

Что такое ООП в Python: основные концепции и преимущества

Объектно-ориентированное программирование (ООП) — это парадигма, позволяющая структурировать код вокруг "объектов", а не функций и логики. В Python, где всё является объектом, эта парадигма реализована особенно элегантно. ООП строится на четырех фундаментальных принципах:

  • Инкапсуляция — объединение данных и методов внутри объекта с контролем доступа к ним
  • Наследование — создание новых классов на основе существующих
  • Полиморфизм — способность объектов разных классов реагировать на одинаковые сообщения по-разному
  • Абстракция — выделение ключевых характеристик объекта, игнорируя несущественные детали

Михаил Васильев, Senior Python Developer

Помню проект, где мы обрабатывали данные из множества источников без ООП. Код превратился в лабиринт из 5000+ строк с дублированием и запутанной логикой. Переписав его с использованием ООП, мы создали иерархию классов для разных источников данных. Базовый класс DataProcessor содержал общую логику, а специфические детали реализовали в дочерних классах через наследование. Результат? Код сократился до 2000 строк, стал поддерживаемым, а новые источники данных интегрировались за часы вместо дней. ООП превратило неуправляемый монолит в элегантную, расширяемую систему.

Преимущества ООП в Python проявляются особенно ярко при работе над средними и крупными проектами:

Преимущество Описание Практическое значение
Модульность Код организован в логические блоки (классы) Проще поддерживать и расширять проект
Повторное использование Классы можно использовать многократно Сокращение объёма кода и времени разработки
Абстракция данных Скрытие внутренней реализации Возможность изменять внутренний код без влияния на внешние вызовы
Коллективная разработка Чёткое разделение ответственности Разные разработчики могут работать над разными классами параллельно

Python реализует ООП с определенными особенностями, делающими его более гибким по сравнению с традиционными объектно-ориентированными языками:

  • Множественное наследование (в отличие от Java)
  • Динамическая типизация — тип объекта определяется во время выполнения
  • Всё является объектом, включая функции и классы
  • "Утиная типизация" — поведение объекта важнее его типа

Освоив основы ООП Python, вы получаете мощный инструментарий для создания масштабируемого, поддерживаемого кода — навык, который отличает любителя от профессионального разработчика. 🧩

Пошаговый план для смены профессии

Классы и объекты в Python: первые шаги

Классы в Python — это чертежи или шаблоны, определяющие структуру и поведение объектов. Объекты же — конкретные экземпляры классов. Представьте класс как схему для постройки дома, а объект — как реальное здание, построенное по этой схеме.

Создать класс в Python просто — используйте ключевое слово class:

Python
Скопировать код
class Car:
# Конструктор класса
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
self.odometer_reading = 0

# Метод класса
def get_descriptive_name(self):
return f"{self.year} {self.make} {self.model}"

# Еще один метод
def read_odometer(self):
return f"This car has {self.odometer_reading} miles on it."

# Метод для изменения состояния объекта
def update_odometer(self, mileage):
if mileage >= self.odometer_reading:
self.odometer_reading = mileage
else:
print("You can't roll back an odometer!")

После определения класса можно создавать его экземпляры — объекты:

Python
Скопировать код
# Создание объекта класса Car
my_car = Car('Audi', 'A4', 2021)

# Использование методов объекта
print(my_car.get_descriptive_name()) # Выведет: 2021 Audi A4
print(my_car.read_odometer()) # Выведет: This car has 0 miles on it.

# Изменение атрибута через метод
my_car.update_odometer(23500)
print(my_car.read_odometer()) # Выведет: This car has 23500 miles on it.

Обратите внимание на следующие ключевые элементы:

  • Метод init — специальный метод, вызываемый автоматически при создании нового экземпляра класса (конструктор)
  • Параметр self — обязательный первый параметр всех методов класса, ссылающийся на создаваемый экземпляр
  • Атрибуты класса — переменные, хранищие состояние объекта (make, model, year, odometer_reading)
  • Методы класса — функции, определяющие поведение объектов этого класса

Анна Соколова, Python-тренер

На курсе для новичков я столкнулась с проблемой: студенты не понимали практическую ценность ООП. Тогда я создала проект "Виртуальный зоопарк". Сначала студенты писали процедурный код для управления животными — получился запутанный массив условных операторов. Затем мы переписали всё через классы: базовый класс Animal с общими свойствами, и дочерние классы Mammal, Bird, Reptile с уникальным поведением. Когда пришло время добавить новый тип животных, один студент сделал это за 5 минут, создав новый класс. Его восторженное "Вау, это реально работает!" показало — концепция ООП наконец-то стала понятной.

Магия классов и объектов Python проявляется в том, как они позволяют моделировать реальный мир в коде. Сравним процедурный подход с объектно-ориентированным для управления банковскими счетами:

Процедурный подход ООП подход
Python
Скопировать код

|

Python
Скопировать код

|

ООП-версия обладает значительными преимуществами:

  • Данные и операции над ними логически сгруппированы вместе
  • Каждый счет — самостоятельный объект, отвечающий за своё состояние
  • Добавление нового функционала (например, вычисление процентов) требует изменений только внутри класса
  • Код становится более читаемым и самодокументируемым

Классы и объекты — основа основ ООП Python, открывающая путь к более сложным концепциям и элегантным решениям. 🏗️

Атрибуты и методы: создание функциональных объектов

Атрибуты и методы — строительные блоки, которые превращают классы из пустых оболочек в функциональные сущности. Атрибуты хранят данные (состояние объекта), а методы определяют поведение (что объект может делать).

В Python существует несколько типов атрибутов:

  • Атрибуты экземпляра — уникальны для каждого объекта, определяются через self
  • Атрибуты класса — общие для всех экземпляров класса
  • Приватные атрибуты — с ограниченным доступом (начинаются с __)

Рассмотрим практический пример, демонстрирующий разные типы атрибутов:

Python
Скопировать код
class Employee:
# Атрибут класса — общий для всех экземпляров
company = "Tech Solutions"

# Счётчик созданных экземпляров
employee_count = 0

def __init__(self, name, salary):
# Атрибуты экземпляра — уникальные для каждого объекта
self.name = name
# Приватный атрибут — условно защищен от внешнего доступа
self.__salary = salary

# Увеличиваем счётчик при создании нового сотрудника
Employee.employee_count += 1

# Генерируем уникальный ID для каждого сотрудника
self.id = f"EMP{Employee.employee_count}"

# Метод для доступа к приватному атрибуту
def get_salary(self):
return self.__salary

# Метод для изменения приватного атрибута с валидацией
def set_salary(self, new_salary):
if new_salary > 0:
self.__salary = new_salary
else:
print("Зарплата должна быть положительным числом")

# Статический метод — не требует экземпляра для вызова
@staticmethod
def company_motto():
return "Инновации для будущего"

# Метод класса — может изменять атрибуты класса
@classmethod
def change_company(cls, new_company):
cls.company = new_company

# Обычный метод экземпляра
def details(self):
return f"ID: {self.id}, Name: {self.name}, Company: {Employee.company}"

Использование этого класса:

Python
Скопировать код
# Создаем сотрудников
employee1 = Employee("Анна", 75000)
employee2 = Employee("Максим", 85000)

# Доступ к атрибутам экземпляра
print(employee1.name) # Анна
print(employee2.name) # Максим

# Доступ к атрибутам класса
print(Employee.company) # Tech Solutions
print(employee1.company) # Tech Solutions – доступно и через экземпляр

# Работа с приватными атрибутами через методы
print(employee1.get_salary()) # 75000
employee1.set_salary(80000)
print(employee1.get_salary()) # 80000

# Прямой доступ к приватному атрибуту не рекомендуется, но технически возможен
# print(employee1.__salary) # AttributeError: 'Employee' object has no attribute '__salary'
print(employee1._Employee__salary) # 80000 – не рекомендуется так делать!

# Использование статического метода
print(Employee.company_motto()) # Инновации для будущего

# Использование метода класса для изменения атрибута класса
Employee.change_company("Global Tech")
print(Employee.company) # Global Tech
print(employee1.company) # Global Tech – изменилось для всех экземпляров

# Использование обычного метода экземпляра
print(employee1.details()) # ID: EMP1, Name: Анна, Company: Global Tech

Методы в Python-классах делятся на несколько типов:

Тип метода Декоратор Первый параметр Назначение
Методы экземпляра self Работа с конкретным экземпляром класса
Методы класса @classmethod cls Работа с классом в целом
Статические методы @staticmethod Функциональность, логически связанная с классом
Магические методы self Определение специального поведения (операторы, конвертация и т.д.)

Магические методы (double underscore methods или dunder methods) заслуживают отдельного внимания — они позволяют объектам взаимодействовать с встроенными функциями и операторами Python:

Python
Скопировать код
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y

# Определяем поведение оператора +
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)

# Строковое представление для пользователя
def __str__(self):
return f"Vector({self.x}, {self.y})"

# Строковое представление для разработчика
def __repr__(self):
return f"Vector({self.x}, {self.y})"

# Поведение при сравнении == 
def __eq__(self, other):
return self.x == other.x and self.y == other.y

# Определяем длину вектора для функции len()
def __len__(self):
return int((self.x**2 + self.y**2)**0.5)

# Делаем объект вызываемым как функцию
def __call__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)

# Использование
v1 = Vector(2, 3)
v2 = Vector(5, 1)

v3 = v1 + v2 # Использует __add__
print(v3) # Вызовет __str__, выведет: Vector(7, 4)

print(v1 == Vector(2, 3)) # True, использует __eq__
print(len(v1)) # 3, использует __len__

v4 = v1(2) # Вызывает объект как функцию через __call__
print(v4) # Vector(4, 6)

Правильное использование атрибутов и методов позволяет создавать интуитивно понятные интерфейсы для ваших классов, делая код более выразительным и легким для понимания. Магические методы — это ещё один уровень элегантности ООП в Python, позволяющий вашим объектам естественно вписываться в язык. 🔮

Наследование, инкапсуляция и полиморфизм на практике

Три столпа ООП — наследование, инкапсуляция и полиморфизм — превращают Python из просто объектно-ориентированного языка в мощный инструмент моделирования реальных систем. Рассмотрим, как эти концепции работают на практике.

Наследование позволяет создавать новые классы на основе существующих, расширяя или изменяя их функциональность:

Python
Скопировать код
# Базовый класс
class Animal:
def __init__(self, name, species):
self.name = name
self.species = species

def make_sound(self):
return "Some generic animal sound"

def info(self):
return f"{self.name} is a {self.species}"

# Дочерний класс, наследующий от Animal
class Dog(Animal):
def __init__(self, name, breed):
# Вызываем конструктор родительского класса
super().__init__(name, species="Dog")
self.breed = breed

# Переопределяем метод родительского класса
def make_sound(self):
return "Woof!"

# Добавляем новый метод, специфичный для собак
def fetch(self, item):
return f"{self.name} fetches the {item}"

# Еще один дочерний класс
class Cat(Animal):
def __init__(self, name, color):
super().__init__(name, species="Cat")
self.color = color

def make_sound(self):
return "Meow!"

def scratch(self):
return f"{self.name} scratches the furniture"

# Использование
animal = Animal("Generic Animal", "Unknown")
dog = Dog("Rex", "German Shepherd")
cat = Cat("Whiskers", "Gray")

print(animal.info()) # Generic Animal is a Unknown
print(dog.info()) # Rex is a Dog
print(cat.info()) # Whiskers is a Cat

print(animal.make_sound()) # Some generic animal sound
print(dog.make_sound()) # Woof!
print(cat.make_sound()) # Meow!

print(dog.fetch("ball")) # Rex fetches the ball
print(cat.scratch()) # Whiskers scratches the furniture

Python также поддерживает множественное наследование, позволяя классу наследоваться от нескольких родителей:

Python
Скопировать код
class FlyingMixin:
def fly(self):
return f"{self.name} is flying high!"

class SwimmingMixin:
def swim(self):
return f"{self.name} is swimming"

class Duck(Animal, FlyingMixin, SwimmingMixin):
def __init__(self, name):
super().__init__(name, species="Duck")

def make_sound(self):
return "Quack!"

duck = Duck("Donald")
print(duck.make_sound()) # Quack!
print(duck.fly()) # Donald is flying high!
print(duck.swim()) # Donald is swimming

Инкапсуляция скрывает внутреннюю реализацию и защищает данные от неконтролируемого доступа. В Python инкапсуляция реализуется через соглашения и механизм свойств:

Python
Скопировать код
class BankAccount:
def __init__(self, owner, initial_balance=0):
self.owner = owner
# Приватный атрибут (соглашение)
self._balance = initial_balance
# Действительно приватный атрибут (затрудняет прямой доступ)
self.__transaction_log = []

# Использование свойства (property) для контролируемого доступа
@property
def balance(self):
return self._balance

# Сеттер для свойства с валидацией
@balance.setter
def balance(self, value):
if not isinstance(value, (int, float)):
raise ValueError("Balance must be a number")
if value < 0:
raise ValueError("Balance cannot be negative")

# Логируем изменение в приватный журнал
self.__log_transaction(self._balance, value)
self._balance = value

# Приватный метод
def __log_transaction(self, old_value, new_value):
import datetime
self.__transaction_log.append({
"timestamp": datetime.datetime.now(),
"old_value": old_value,
"new_value": new_value
})

# Публичный метод для доступа к приватным данным
def get_transaction_history(self):
return self.__transaction_log.copy() # Возвращаем копию для безопасности

account = BankAccount("Alex", 1000)
print(account.balance) # 1000

# Используем сеттер свойства с валидацией
try:
account.balance = -500 # Вызовет ошибку
except ValueError as e:
print(str(e)) # Balance cannot be negative

account.balance = 1500
print(account.balance) # 1500

# Доступ к приватному атрибуту затруднен, но не невозможен
# print(account.__transaction_log) # AttributeError
# Но все еще можно получить доступ через name mangling (не рекомендуется)
# print(account._BankAccount__transaction_log)

# Вместо этого используем публичный метод
print(account.get_transaction_history())

Полиморфизм позволяет объектам разных классов реагировать на один и тот же метод по-разному. Это обеспечивает гибкость и упрощает код:

Python
Скопировать код
# Функция, демонстрирующая полиморфизм
def animal_sounds(animals):
for animal in animals:
# Один и тот же метод работает для разных типов объектов
print(f"{animal.name} says {animal.make_sound()}")

animal_sounds([
Animal("Generic", "Unknown"),
Dog("Rex", "German Shepherd"),
Cat("Whiskers", "Gray"),
Duck("Donald")
])
# Generic says Some generic animal sound
# Rex says Woof!
# Whiskers says Meow!
# Donald says Quack!

# Полиморфизм через протокол — утиная типизация
def process_sequence(sequence):
# Работает с любым объектом, поддерживающим len() и индексацию
print(f"Длина: {len(sequence)}")
print(f"Первый элемент: {sequence[0]}")
print(f"Обход в цикле:")
for item in sequence:
print(f" – {item}")

# Работает с разными типами последовательностей
process_sequence([1, 2, 3])
process_sequence("Python")
process_sequence({"a": 1, "b": 2, "c": 3})

Мощь ООП в Python раскрывается, когда эти три концепции работают вместе, создавая гибкие и расширяемые структуры кода. Например, при разработке системы управления библиотекой:

Python
Скопировать код
class LibraryItem:
def __init__(self, title, item_id):
self.title = title
self.item_id = item_id
self._checked_out = False

@property
def checked_out(self):
return self._checked_out

def check_out(self):
if self._checked_out:
return False
self._checked_out = True
return True

def check_in(self):
if not self._checked_out:
return False
self._checked_out = False
return True

def display_info(self):
status = "checked out" if self._checked_out else "available"
return f"{self.title} (ID: {self.item_id}) – {status}"

class Book(LibraryItem):
def __init__(self, title, item_id, author, pages):
super().__init__(title, item_id)
self.author = author
self.pages = pages

def display_info(self):
basic_info = super().display_info()
return f"{basic_info}\nAuthor: {self.author}, Pages: {self.pages}"

class DVD(LibraryItem):
def __init__(self, title, item_id, director, runtime):
super().__init__(title, item_id)
self.director = director
self.runtime = runtime

def display_info(self):
basic_info = super().display_info()
return f"{basic_info}\nDirector: {self.director}, Runtime: {self.runtime} min"

# Демонстрация работы
book = Book("The Hobbit", "B12345", "J.R.R. Tolkien", 366)
dvd = DVD("Inception", "D67890", "Christopher Nolan", 148)

print(book.display_info())
book.check_out()
print(book.display_info())

print("\n" + dvd.display_info())
dvd.check_out()
print(dvd.display_info())

# Полиморфная функция для обработки любых библиотечных элементов
def process_items(items):
for item in items:
print("\nProcessing item:")
print(item.display_info())

process_items([book, dvd])

Эти примеры демонстрируют, как наследование, инкапсуляция и полиморфизм создают мощный фундамент для построения сложных, но гибких и поддерживаемых программных систем. Именно эти концепции делают ООП таким ценным инструментом в арсенале Python-разработчика. 🧠

Практические задания по ООП на Python: закрепляем знания

Теория без практики бесполезна, особенно в программировании. Давайте закрепим знания об основах ООП Python через серию практических заданий, постепенно наращивая сложность. 💪

Задание 1: Создание простого класса

Создайте класс Rectangle, который будет представлять прямоугольник с методами для расчета площади и периметра.

Python
Скопировать код
class Rectangle:
def __init__(self, length, width):
self.length = length
self.width = width

def area(self):
return self.length * self.width

def perimeter(self):
return 2 * (self.length + self.width)

# Проверка
rectangle = Rectangle(5, 3)
print(f"Площадь: {rectangle.area()}") # Должно вывести 15
print(f"Периметр: {rectangle.perimeter()}") # Должно вывести 16

Задание 2: Инкапсуляция и валидация данных

Доработайте класс Rectangle, добавив защиту от отрицательных значений сторон и возможность изменения размеров.

Python
Скопировать код
class Rectangle:
def __init__(self, length, width):
self._length = 0 # Защищенные атрибуты
self._width = 0
# Используем сеттеры для начальной установки значений
self.length = length
self.width = width

@property
def length(self):
return self._length

@length.setter
def length(self, value):
if value <= 0:
raise ValueError("Длина должна быть положительным числом")
self._length = value

@property
def width(self):
return self._width

@width.setter
def width(self, value):
if value <= 0:
raise ValueError("Ширина должна быть положительным числом")
self._width = value

def area(self):
return self.length * self.width

def perimeter(self):
return 2 * (self.length + self.width)

# Проверка
rect = Rectangle(5, 3)
print(f"Площадь: {rect.area()}") # 15

# Изменение размеров
rect.length = 7
rect.width = 4
print(f"Новая площадь: {rect.area()}") # 28

try:
rect.length = -2 # Должно вызвать ошибку
except ValueError as e:
print(f"Ошибка: {e}") # Ошибка: Длина должна быть положительным числом

Задание 3: Наследование и полиморфизм

Создайте иерархию геометрических фигур: базовый класс Shape и производные классы Circle, Rectangle, и Triangle.

Python
Скопировать код
import math

class Shape:
def area(self):
"""Вычисляет площадь фигуры."""
raise NotImplementedError("Подклассы должны реализовать этот метод")

def perimeter(self):
"""Вычисляет периметр фигуры."""
raise NotImplementedError("Подклассы должны реализовать этот метод")

def description(self):
"""Возвращает описание фигуры."""
return f"{self.__class__.__name__}: площадь = {self.area()}, периметр = {self.perimeter()}"

class Circle(Shape):
def __init__(self, radius):
self.radius = radius

def area(self):
return math.pi * self.radius ** 2

def perimeter(self):
return 2 * math.pi * self.radius

class Rectangle(Shape):
def __init__(self, length, width):
self.length = length
self.width = width

def area(self):
return self.length * self.width

def perimeter(self):
return 2 * (self.length + self.width)

class Triangle(Shape):
def __init__(self, a, b, c):
# Проверка неравенства треугольника
if a + b <= c or a + c <= b or b + c <= a:
raise ValueError("Нарушено неравенство треугольника")
self.a = a
self.b = b
self.c = c

def area(self):
# Формула Герона
s = (self.a + self.b + self.c) / 2
return math.sqrt(s * (s – self.a) * (s – self.b) * (s – self.c))

def perimeter(self):
return self.a + self.b + self.c

# Демонстрация полиморфизма
shapes = [
Circle(5),
Rectangle(4, 6),
Triangle(3, 4, 5)
]

for shape in shapes:
print(shape.description())

Задание 4: Композиция и агрегация

Создайте систему классов, моделирующую автомобиль, состоящий из различных компонентов.

Python
Скопировать код
class Engine:
def __init__(self, power, volume):
self.power = power
self.volume = volume
self._running = False

def start(self):
if not self._running:
self._running = True
return "Двигатель запущен"
return "Двигатель уже работает"

def stop(self):
if self._running:
self._running = False
return "Двигатель остановлен"
return "Двигатель уже остановлен"

@property
def status(self):
return "работает" if self._running else "остановлен"

class FuelTank:
def __init__(self, capacity):
self.capacity = capacity
self._fuel_level = 0

def fill(self, amount):
if amount <= 0:
return "Количество должно быть положительным"

if self._fuel_level + amount > self.capacity:
actual_fill = self.capacity – self._fuel_level
self._fuel_level = self.capacity
return f"Бак заполнен. Добавлено {actual_fill} л. (не поместилось {amount – actual_fill} л.)"

self._fuel_level += amount
return f"Добавлено {amount} л. топлива"

@property
def fuel_level(self):
return self._fuel_level

class Car:
def __init__(self, make, model, engine_power, engine_volume, tank_capacity):
self.make = make
self.model = model
# Композиция – создаем компоненты внутри класса
self.engine = Engine(engine_power, engine_volume)
self.fuel_tank = FuelTank(tank_capacity)

def start_engine(self):
if self.fuel_tank.fuel_level > 0:
return self.engine.start()
return "Недостаточно топлива"

def stop_engine(self):
return self.engine.stop()

def add_fuel(self, amount):
return self.fuel_tank.fill(amount)

def status(self):
return f"{self.make} {self.model}:\n" \
f"Двигатель: {self.engine.power} л.с., {self.engine.volume} л, {self.engine.status}\n" \
f"Топливный бак: {self.fuel_tank.fuel_level}/{self.fuel_tank.capacity} л"

# Использование
my_car = Car("Toyota", "Camry", 180, 2.5, 60)
print(my_car.status())

print(my_car.add_fuel(45))
print(my_car.start_engine())
print(my_car.status())

print(my_car.stop_engine())
print(my_car.status())

Задание 5: Создание полноценной системы классов

Разработайте мини-систему управления библиотекой с классами для книг, читателей и самой библиотеки. Реализуйте выдачу книг, их возврат и отслеживание сроков.

Python
Скопировать код
import datetime

class Book:
def __init__(self, title, author, isbn):
self.title = title
self.author = author
self.isbn = isbn
self._checked_out = False
self._checked_out_by = None
self._due_date = None

@property
def is_checked_out(self):
return self._checked_out

@property
def checked_out_info(self):
if not self._checked_out:
return "Книга доступна"

days_overdue = (datetime.date.today() – self._due_date).days if self._due_date else 0

if days_overdue > 0:
return f"Выдана {self._checked_out_by.name}, просрочена на {days_overdue} дней"
else:
return f"Выдана {self._checked_out_by.name}, вернуть до {self._due_date}"

def __str__(self):
return f"'{self.title}' by {self.author} (ISBN: {self.isbn})"

class Patron:
def __init__(self, name, patron_id):
self.name = name
self.patron_id = patron_id
self._checked_out_books = []

def add_book(self, book):
self._checked_out_books.append(book)

def remove_book(self, book):
if book in self._checked_out_books:
self._checked_out_books.remove(book)

@property
def checked_out_books(self):
return self._checked_out_books.copy() # Возвращаем копию для защиты

def __str__(self):
return f"{self.name} (ID: {self.patron_id}), книг на руках: {len(self._checked_out_books)}"

class Library:
def __init__(self, name):
self.name = name
self.books = []
self.patrons = []

def add_book(self, book):
self.books.append(book)
return f"Книга {book} добавлена в библиотеку {self.name}"

def add_patron(self, patron):
self.patrons.append(patron)
return f"Читатель {patron.name} зарегистрирован в библиотеке"

def check_out_book(self, book_isbn, patron_id, days=14):
# Находим книгу и читателя
book = next((b for b in self.books if b.isbn == book_isbn), None)
patron = next((p for p in self.patrons if p.patron_id == patron_id), None)

if not book:
return "Книга не найдена"

if not patron:
return "Читатель не найден"

if book.is_checked_out:
return f"Книга {book} уже выдана"

# Устанавливаем информацию о выдаче
book._checked_out = True
book._checked_out_by = patron
book._due_date = datetime.date.today() + datetime.timedelta(days=days)

# Добавляем книгу в список выданных читателю
patron.add_book(book)

return f"Книга {book} выдана читателю {patron.name} до {book._due_date}"

def return_book(self, book_isbn):
book = next((b for b in self.books if b.isbn == book_isbn), None)

if not book:
return "Книга не найдена"

if not book.is_checked_out:
return f"Книга {book} не была выдана"

# Удаляем книгу из списка выданных читателю
patron = book._checked_out_by
patron.remove_book(book)

# Сбрасываем информацию о выдаче
book._checked_out = False
book._checked_out_by = None
book._due_date = None

return f"Книга {book} возвращена в библиотеку"

def get_overdue_books(self):
today = datetime.date.today()
overdue_books = [book for book in self.books 
if book.is_checked_out and book._due_date < today]
return overdue_books

# Демонстрация работы
library = Library("Городская библиотека")

# Добавляем книги
book1 = Book("Война и мир", "Лев Толстой", "123456")
book2 = Book("Преступление и наказание", "Федор Достоевский", "789012")
book3 = Book("Мастер и Маргарита", "Михаил Булгаков", "345678")

library.add_book(book1)
library.add_book(book2)
library.add_book(book3)

# Регистрируем читателей
patron1 = Patron("Иван Иванов", "P001")
patron2 = Patron("Анна Смирнова", "P002")

library.add_patron(patron1)
library.add_patron(patron2)

# Выдаем книги
print(library.check_out_book("123456", "P001"))
print(library.check_out_book("789012", "P002"))

# Проверяем статус
print(book1.checked_out_info)
print(patron1)

# Возвращаем книгу
print(library.return_book("123456"))
print(book1.checked_out_info)

Попробуйте выполнить эти задания самостоятельно, экспериментируя с различными подходами и решениями. Это лучший способ закрепить понимание основ ООП Python и развить навыки объектно-ориентированного проектирования.

Усложните задачи, добавив собственные требования и функциональность. Например:

  • Добавьте дополнительные методы для геометрических фигур (вращение, масштабирование)
  • Расширьте модель автомобиля, добавив трансмиссию, колеса, тормозную систему
  • Добавьте систему штрафов за просроченные книги в библиотечной системе
  • Реализуйте поддержку бронирования книг и очереди читателей

Создание собственных классов и проектирование их взаимодействия — это лучший путь к мастерству в ООП. Чем больше вы практикуетесь, тем более естественным становится процесс объектно-ориентированного мышления. 🚀

Объектно-ориентированное программирование в Python — это не просто набор синтаксических конструкций, а образ мышления, который трансформирует подход к решению задач. Освоив базовые принципы ООП — классы, объекты, наследование, инкапсуляцию и полиморфизм — вы получаете мощные инструменты для создания гибкого, модульного и поддерживаемого кода. Регулярная практика и эксперименты с различными паттернами проектирования сделают ООП вашим естественным подходом к разработке. И помните: лучшие программисты не те, кто пишет сложный код, а те, кто умеет сделать сложные вещи простыми и понятными через грамотное проектирование.

Читайте также

Проверь как ты усвоил материалы статьи
Пройди тест и узнай насколько ты лучше других читателей
Что такое инкапсуляция в ООП?
1 / 5

Загрузка...