Практикум закрепляет наследование (урок 70) и множественное наследование (урок 72). Примеры идут от простого к сложному: сначала базовый механизм super(), затем узкий случай практикума — чеки и уведомления, затем ловушки множественного наследования. Весь вывод в комментариях получен реальным запуском кода.
Пример 1. Базовое наследование: super().__init__() и переопределение метода
Наследник вызывает super().__init__(), чтобы не повторять код родителя, и переопределяет метод, чтобы расширить, а не заменить его поведение.
class Employee:
def __init__(self, name: str, salary: float) -> None:
self.name = name
self.salary = salary
def describe(self) -> str:
return f"{self.name}: {self.salary}"
class Manager(Employee):
def __init__(self, name: str, salary: float, team_size: int) -> None:
super().__init__(name, salary) # переиспользуем __init__ родителя
self.team_size = team_size
def describe(self) -> str:
base = super().describe() # достраиваем базовую реализацию
return f"{base}, team of {self.team_size}"
employee = Employee("Alice", 50000.0)
manager = Manager("Bob", 80000.0, 5)
print(employee.describe()) # Alice: 50000.0
print(manager.describe()) # Bob: 80000.0, team of 5
Что происходит: Manager.__init__ не копирует присвоение name/salary — эту работу делает super().__init__(name, salary). В describe тот же приём: super().describe() возвращает готовую строку родителя, и наследник только дописывает к ней информацию о команде.
Пример 2. Валидация в конструкторе наследника
Наследник может добавить проверку данных до вызова super().__init__() — так недопустимый объект не будет создан вообще. Это основная задача практикума: SaleReceipt требует положительную сумму, ReturnReceipt — отрицательную.
class Receipt:
def __init__(self, receipt_id: int, amount: float) -> None:
self.id = receipt_id
self.amount = amount
def __str__(self) -> str:
return f"{self.__class__.__name__} {self.id}: {self.amount}"
class SaleReceipt(Receipt):
def __init__(self, receipt_id: int, amount: float) -> None:
if amount <= 0:
raise ValueError("SaleReceipt amount must be positive.")
super().__init__(receipt_id, amount)
def __str__(self) -> str:
return f"{self.__class__.__name__} {self.id}: +{self.amount}"
class ReturnReceipt(Receipt):
def __init__(self, receipt_id: int, amount: float) -> None:
if amount >= 0:
raise ValueError("ReturnReceipt amount must be negative.")
super().__init__(receipt_id, amount)
def __str__(self) -> str:
return f"{self.__class__.__name__} {self.id}: {self.amount}"
sale = SaleReceipt(1, 100.0)
ret = ReturnReceipt(2, -50.0)
print(sale) # SaleReceipt 1: +100.0
print(ret) # ReturnReceipt 2: -50.0
try:
SaleReceipt(3, -10.0)
except ValueError as error:
print("ValueError:", error)
# ValueError: SaleReceipt amount must be positive.
Что происходит: проверка идёт раньше super().__init__(...) — если сумма невалидна, raise прерывает конструктор, и объект Receipt вообще не создаётся, поле amount ни на миг не окажется невалидным.
Пример 3. isinstance для фильтрации разнородных объектов
isinstance принимает не только один класс, но и кортеж классов — тогда проверка отвечает «объект хотя бы одного из этих типов». На этом строится подсчёт итогов смены по типу чека.
class Shift:
_id_counter = 1
def __init__(self) -> None:
self.id = Shift._id_counter
Shift._id_counter += 1
self.receipts: list[Receipt] = []
self.closed = False
def close(self) -> None:
self.closed = True
def add_receipt(self, amount: float) -> None:
if self.closed:
raise ValueError("Cannot add receipt to closed shift.")
receipt_id = len(self.receipts) + 1
self.receipts.append(SaleReceipt(receipt_id, amount))
def add_return(self, amount: float) -> None:
if self.closed:
raise ValueError("Cannot add receipt to closed shift.")
receipt_id = len(self.receipts) + 1
self.receipts.append(ReturnReceipt(receipt_id, amount))
def get_total(self, receipt_type: type | tuple[type, ...] | None = None) -> float:
if receipt_type is None:
return sum(r.amount for r in self.receipts)
return sum(r.amount for r in self.receipts if isinstance(r, receipt_type))
shift = Shift()
shift.add_receipt(100.0)
shift.add_receipt(250.0)
shift.add_return(-40.0)
print(shift.get_total()) # 310.0
print(shift.get_total(SaleReceipt)) # 350.0
print(shift.get_total(ReturnReceipt)) # -40.0
print(shift.get_total((SaleReceipt, ReturnReceipt))) # 310.0 — кортеж типов
mixed = [SaleReceipt(1, 10.0), ReturnReceipt(2, -5.0), "not a receipt", 42]
only_receipts = [item for item in mixed if isinstance(item, (SaleReceipt, ReturnReceipt))]
print(len(only_receipts)) # 2
Что происходит: get_total(SaleReceipt) пропускает только продажи, а get_total((SaleReceipt, ReturnReceipt)) пропускает и то, и другое — та же проверка, что и без аргумента, но записанная явно через типы. Последняя строка показывает более общий приём: отбор нужных объектов из списка вперемешку со строками и числами.
Пример 4. hasattr — защитная проверка перед доступом к атрибуту
hasattr(obj, "attr") проверяет наличие атрибута, не поднимая исключение. Полезно, когда объект мог быть создан без необязательного поля.
class Config:
def __init__(self, timeout: float) -> None:
self.timeout = timeout
def describe_config(config: Config) -> str:
if not hasattr(config, "retries"):
return f"timeout={config.timeout}, retries=default"
return f"timeout={config.timeout}, retries={config.retries}"
basic_config = Config(30.0)
print(describe_config(basic_config)) # timeout=30.0, retries=default
basic_config.retries = 3
print(describe_config(basic_config)) # timeout=30.0, retries=3
Что происходит: Config не объявляет retries в __init__, поэтому обращение config.retries без проверки упало бы с AttributeError. hasattr заранее отвечает, есть ли атрибут, и функция выбирает безопасную ветку. Этот же приём в примере 6 защищает email_notify и sms_notify от чтения несуществующих полей.
Пример 5. Множественное наследование и порядок разрешения методов (MRO)
Когда класс наследует от нескольких родителей, Python выстраивает их в линейный порядок поиска — MRO (Method Resolution Order), доступный через Class.__mro__. super() идёт не «к родителю», а к следующему классу именно в этом порядке.
class Base:
def ping(self) -> str:
return "Base.ping"
class Left(Base):
def ping(self) -> str:
return "Left.ping -> " + super().ping()
class Right(Base):
def ping(self) -> str:
return "Right.ping -> " + super().ping()
class Diamond(Left, Right):
pass
print(Diamond.__mro__)
# (<class '__main__.Diamond'>, <class '__main__.Left'>, <class '__main__.Right'>, <class '__main__.Base'>, <class 'object'>)
print(Diamond().ping())
# Left.ping -> Right.ping -> Base.ping
Что происходит: у Diamond нет своего ping, поэтому вызывается Left.ping — он в MRO первым после Diamond. Внутри Left.ping вызов super().ping() идёт не сразу к Base, а к следующему в MRO — Right, и только потом к Base. Так решается «проблема ромба»: каждый класс цепочки отрабатывает один раз, в едином порядке.
Пример 6. Миксины: добавляем поведение через уведомления
Миксин — класс, который не создаётся сам по себе, а подмешивается к другому классу через множественное наследование, добавляя набор методов. Один и тот же User получает разный набор способов уведомления в зависимости от того, какие миксины к нему подмешаны.
class EmailNotifyMixin:
def email_notify(self, message: str) -> None:
if not hasattr(self, "email"):
raise AttributeError("Missing 'email'")
print(f"Email to {self.email}: {message}")
class SmsNotifyMixin:
def sms_notify(self, message: str) -> None:
if not hasattr(self, "phone"):
raise AttributeError("Missing 'phone'")
print(f"SMS to {self.phone}: {message}")
class PushNotifyMixin:
def push_notify(self, message: str) -> None:
print(f"Push notification: {message}")
class User:
def __init__(self, name: str) -> None:
self.name = name
class Courier(User, EmailNotifyMixin, SmsNotifyMixin):
def __init__(self, name: str, email: str, phone: str) -> None:
super().__init__(name)
self.email = email
self.phone = phone
def notify(self, message: str) -> None:
self.email_notify(message)
self.sms_notify(message)
class Admin(User, PushNotifyMixin):
def notify(self, message: str) -> None:
self.push_notify(message)
courier = Courier("Alice", "alice@example.com", "+123456789")
courier.notify("Package delivered.")
# Email to alice@example.com: Package delivered.
# SMS to +123456789: Package delivered.
admin = Admin("Bob")
admin.notify("New report available.")
# Push notification: New report available.
Что происходит: Courier и Admin оба наследуют от User, но набор миксинов у них разный, поэтому и метод notify у каждого свой. hasattr внутри миксинов не даёт вызвать уведомление у объекта, которому забыли задать email или phone.
Пример 7. Ловушка миксина: обязательное поле в __init__
Если __init__ миксина требует обязательный аргумент, а класс, который его подмешивает, не гарантирует вызов этого __init__ — поле останется не установленным. Безопаснее давать полю значение по умолчанию и вызывать super().__init__().
class BrokenAuditMixin:
def __init__(self, actor: str) -> None:
self.actor = actor
def audit(self) -> None:
print(f"Audited by {self.actor}")
class Widget:
def __init__(self, title: str) -> None:
self.title = title # не вызывает super().__init__() — цепочка обрывается
class BadPanel(Widget, BrokenAuditMixin):
def __init__(self, title: str) -> None:
super().__init__(title) # уходит в Widget.__init__, actor не передаётся
bad_panel = BadPanel("Dashboard")
try:
bad_panel.audit()
except AttributeError as error:
print("AttributeError:", error)
# AttributeError: 'BadPanel' object has no attribute 'actor'
class GoodAuditMixin:
def __init__(self, actor: str = "system") -> None: # значение по умолчанию
super().__init__()
self.actor = actor
def audit(self) -> None:
print(f"Audited by {self.actor}")
class GoodPanel(GoodAuditMixin):
def __init__(self, title: str) -> None:
super().__init__() # actor получит значение по умолчанию "system"
self.title = title
good_panel = GoodPanel("Dashboard")
good_panel.audit() # Audited by system
print(good_panel.title) # Dashboard
Что происходит: в BadPanel вызов super().__init__(title) по MRO попадает в Widget.__init__, а тот не вызывает super().__init__() дальше — BrokenAuditMixin.__init__ не выполняется, и self.actor никогда не создаётся. В GoodPanel у миксина есть значение по умолчанию и собственный super().__init__(), поэтому цепочка не ломается, даже если наследник не передал actor явно.
Пример 8. Ловушка MRO: порядок родителей меняет вызываемый метод
Если у двух родителей есть метод с одинаковым именем, побеждает тот, что стоит в объявлении класса раньше — это прямое следствие MRO, а не случайность.
class LoudMixin:
def greet(self) -> str:
return "LOUD HELLO"
class QuietMixin:
def greet(self) -> str:
return "quiet hello"
class SpeakerA(LoudMixin, QuietMixin):
pass
class SpeakerB(QuietMixin, LoudMixin):
pass
print(SpeakerA.__mro__)
# (..., <class '__main__.LoudMixin'>, <class '__main__.QuietMixin'>, ...)
print(SpeakerA().greet()) # LOUD HELLO — первый по MRO родитель победил
print(SpeakerB.__mro__)
# (..., <class '__main__.QuietMixin'>, <class '__main__.LoudMixin'>, ...)
print(SpeakerB().greet()) # quiet hello — родители поменяны местами
Что происходит: у SpeakerA и SpeakerB одинаковый набор родителей, но в разном порядке — и это единственная разница между классами. greet() ищется по MRO слева направо, поэтому побеждает метод того класса, что записан в скобках первым. Если такое поведение нежелательно, конфликт имён нужно решать явно: переименовать метод в одном из миксинов или переопределить greet в самом наследнике.
Пример 9. Всё вместе: смена, чеки и уведомления
Финальный пример практикума: закрытая смена запрещает добавлять чеки, а курьер и админ используют те же классы с миксинами, что и в примере 6.
evening_shift = Shift()
evening_shift.add_receipt(75.0)
evening_shift.add_return(-15.0)
evening_shift.close()
print(evening_shift.get_total()) # 60.0
try:
evening_shift.add_receipt(10.0)
except ValueError as error:
print("ValueError:", error)
# ValueError: Cannot add receipt to closed shift.
alice = Courier("Alice", "alice@example.com", "+123456789")
alice.notify("Package delivered.")
bob = Admin("Bob")
bob.notify("New report available.")
Что происходит: close() ставит флаг, и guard clause в начале add_receipt прерывает выполнение раньше, чем чек попал бы в список. Это тот же приём защитной проверки, что и hasattr в примере 4 — только условие проверяет не наличие атрибута, а состояние объекта.
Что делать дальше
Переходите к заданиям — там те же две задачи практикума пошагово. Если чек или уведомление ведут себя не так, как ожидалось, — загляните в типичные ошибки. Тему миксинов и MRO подробнее разбирает урок 72, а инкапсуляцию полей вроде email/phone — следующий урок 74.