📜 Множественное наследование 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.