⚖️ Старый и современный подход

⚡ Различия в двух словах

Старый: множественное наследование без аннотаций, явный вызов родителя, неясные границы между классами.

Новый: аннотации типов, super() вместо жёсткой привязки, предпочтение композиции там, где наследование усложняет код.

Вывод: современный код читать проще, проще поддерживать и меньше риск ошибок в MRO.

📜 Множественное наследование vs композиция

Исходный материал показывает, как собирать классы через множественное наследование. Это работает, но при росте проекта такие иерархии быстро становятся сложными.

Наследование

class Flyable:
    def fly(self) -> None:
        print("Flying")


class Swimmable:
    def swim(self) -> None:
        print("Swimming")


class Duck(Flyable, Swimmable):
    pass


duck = Duck()
duck.fly()
duck.swim()

Проблема: если появятся конфликтующие методы или общий предок, придётся разбираться с MRO. Класс Duck жёстко привязан к обоим родителям.

Композиция

class Flyable:
    def fly(self) -> None:
        print("Flying")


class Swimmable:
    def swim(self) -> None:
        print("Swimming")


class Duck:
    def __init__(self) -> None:
        self.flyable = Flyable()
        self.swimmable = Swimmable()

    def fly(self) -> None:
        self.flyable.fly()

    def swim(self) -> None:
        self.swimmable.swim()


duck = Duck()
duck.fly()
duck.swim()

Преимущество: Duck не зависит от иерархии наследования. Поведение легко заменить или расширить без MRO.

❌ Явный вызов родителя vs ✅ super()

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

Явный вызов

class Base:
    def action(self) -> None:
        print("Base")


class Child(Base):
    def action(self) -> None:
        print("Child")
        Base.action(self)  # жёсткая привязка к Base

Почему плохо: если позже Child начнёт наследовать от другого класса между Child и Base, вызов всё равно пойдёт в Base, а не в следующий по MRO.

super()

class Base:
    def action(self) -> None:
        print("Base")


class Child(Base):
    def action(self) -> None:
        print("Child")
        super().action()  # следующий метод в MRO

Почему хорошо: super() учитывает актуальный MRO класса. При добавлении новых родителей или миксинов поведение остаётся предсказуемым.

❌ Почему сложные иерархии наследования могут быть не лучшим выбором

  • Каждый новый родитель увеличивает риск конфликтов имён и непредсказуемого MRO.
  • Класс становится жёстко связан с деталями реализации родителей.
  • Без аннотаций типов сложно понять, какие атрибуты ожидает миксин.
  • Миксины с состоянием (__init__) могут конфликтовать при множественном наследовании.

✅ Рекомендуемый современный вариант

Современный Python предлагает использовать множественное наследование осознанно: для миксинов без состояния и для чётких иерархий. В остальных случаях — композицию и аннотации типов.

class LoggableMixin:
    def log(self, message: str) -> None:
        print(f"[LOG] {message}")


class Repository:
    def save(self, data: str) -> None:
        self.log(f"Saving {data}")


class UserRepository(Repository, LoggableMixin):
    pass


repo = UserRepository()
repo.save("user data")

Что улучшилось:

  • LoggableMixin не имеет состояния и добавляет только поведение.
  • Аннотации типов делают интерфейс понятным.
  • Основная логика хранится в Repository, а логирование — в миксине.
  • Композиция используется, когда «является» звучит неестественно.

🕰️ Когда старый подход ещё можно встретить

В legacy-проектах и учебных материалах часто встречаются классы без аннотаций и с явным вызовом родителей. Главное — распознать такой код и понимать MRO. При рефакторинге заменяйте явные вызовы на super(), добавляйте типы и рассматривайте композицию как альтернативу глубоким иерархиям.

Проверить по документации: про super() и MRO см. super() и Multiple Inheritance. Аннотации типов — в typing и PEP 484.