💻 Примеры

⚡ Пять инструментов наследования на одном примере

class Receipt:
    def __init__(self, rid, amount):
        self.id, self.amount = rid, amount
class SaleReceipt(Receipt):
    def __init__(self, rid, amount):
        super().__init__(rid, amount)  # переиспользуем родителя
sale = SaleReceipt(1, 100.0)
print(isinstance(sale, Receipt), sale.amount)  # True 100.0
ИнструментЧто делает
super().__init__()вызывает конструктор родителя
isinstance(obj, Cls)проверка типа, принимает кортеж классов
Class.__mro__порядок поиска метода по родителям
миксиныповедение сразу от нескольких родителей
hasattr(obj, "attr")проверка перед доступом к атрибуту
Топ-3 ошибки: забыть super().__init__() в наследнике · перепутать порядок родителей при миксинах · не проверить сумму до super().

Практикум закрепляет наследование (урок 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.