Перейти к содержимому

Практика 4. Практическая работа 4. Множественное наследование и миксины

Цель

Научиться осознанно применять множественное наследование в Python: строить иерархии классов, понимать и вычислять порядок разрешения методов (MRO, __mro__), проектировать и подключать миксины, а также организовывать кооперативные цепочки вызовов через super(). По итогам работы вы должны уметь предсказывать порядок выполнения методов и объяснять его на основе C3-линеаризации.

Краткая теория

Множественное наследование — класс наследуется сразу от нескольких базовых классов, перечисленных через запятую: class D(B, C). Порядок перечисления значим.

MRO (Method Resolution Order) — линейный список классов, по которому Python ищет метод или атрибут. Доступен через Cls.__mro__ (кортеж) и Cls.mro() (список). Поиск идёт слева направо, берётся первая найденная реализация. В конце списка всегда стоит object.

MRO вычисляется алгоритмом C3-линеаризации, который гарантирует: потомок раньше родителей, сохранение локального порядка базовых классов и монотонность. Если согласованный порядок построить нельзя, Python возбуждает TypeError ещё на этапе определения класса.

super() обращается не к «родителю», а к следующему классу по MRO относительно текущего и типа объекта. Это делает возможным кооперативное наследование: каждый класс вызывает super().__init__(**kwargs) (или другой метод) ровно один раз и передаёт неиспользованные аргументы дальше. Так каждый класс в цепочке отрабатывает один раз, а «проблема ромба» (двойной вызов общего предка) исчезает.

Миксин (Mixin) — класс, который не используется самостоятельно, а «подмешивает» узкое переиспользуемое поведение (логирование, сериализация, временные метки). Соглашения: суффикс Mixin в имени, узкая ответственность, размещение слева от основного базового класса, кооперативность через super().


Задания

Решения собирайте в solution.py (или пакет solution/). Добавляйте аннотации типов и осмысленные __repr__. Где требуется — сопровождайте код короткими автотестами на pytest.

Задание 1. Способности через миксины

Реализуйте независимые «способности» как отдельные классы и соберите из них персонажа.

Требования:

  • Классы FlyableMixin (метод fly() -> str), SwimmableMixin (swim() -> str), WalkableMixin (walk() -> str). Каждый возвращает короткую строку-описание.
  • Класс Amphibian(WalkableMixin, SwimmableMixin) и класс Duck(FlyableMixin, SwimmableMixin, WalkableMixin).
  • Метод Duck.describe() -> str, который объединяет все способности в одну строку.
  • Выведите [c.__name__ for c in Duck.__mro__] и объясните в комментарии, почему миксины расположены именно в таком порядке.

Задание 2. Разбор MRO и «ромб»

Дана классическая ромбовидная иерархия:

class A:
def who(self) -> str:
return "A"
class B(A):
def who(self) -> str:
return "B -> " + super().who()
class C(A):
def who(self) -> str:
return "C -> " + super().who()
class D(B, C):
def who(self) -> str:
return "D -> " + super().who()

Требования:

  • До запуска кода запишите в комментарии свой прогноз: что вернёт D().who() и каким будет D.__mro__.
  • Затем выполните код и сравните с прогнозом.
  • Постройте вторую иерархию class E(C, B) и объясните, чем её MRO отличается от D(B, C).
  • В отдельном комментарии вручную примените правило merge C3-линеаризации для L[D] и приложите пошаговый вывод.

Задание 3. Предскажи порядок вызовов (кооперативный super())

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

class Base:
def __init__(self, **kwargs):
super().__init__()
print("Base.__init__")
class Engine(Base):
def __init__(self, *, power: int, **kwargs):
super().__init__(**kwargs)
self.power = power
print(f"Engine.__init__ (power={power})")
class Radio(Base):
def __init__(self, *, band: str, **kwargs):
super().__init__(**kwargs)
self.band = band
print(f"Radio.__init__ (band={band})")
class Car(Engine, Radio):
def __init__(self, *, name: str, **kwargs):
super().__init__(**kwargs)
self.name = name
print(f"Car.__init__ (name={name})")
car = Car(name="Tesla", power=500, band="FM")

Требования:

  • Запишите прогноз: Car.__mro__ и точную последовательность напечатанных строк.
  • Запустите код, сравните с прогнозом, объясните расхождения (если есть).
  • Эксперимент: замените в Engine.__init__ вызов super().__init__(**kwargs) на прямой Base.__init__(self). Что изменится в выводе и почему? Опишите словами, какая часть цепочки «выпадает».

Задание 4. Кооперативные миксины — конвейер

Реализуйте «конвейер сохранения», где миксины добавляют поведение, не заменяя базовую операцию.

Требования:

  • Базовый класс Repository с методом save(self, data: str) -> None, выполняющим «сохранение» (печать). Он завершает цепочку и не вызывает super().save.
  • ValidationMixin.save — проверяет, что данные непустые, иначе raise ValueError; затем вызывает super().save(data).
  • AuditMixin.save — пишет в журнал факт попытки сохранения, затем super().save(data).
  • Класс UserRepository(AuditMixin, ValidationMixin, Repository).
  • Продемонстрируйте порядок «аудит → валидация → сохранение», выведите __mro__ и проверьте поведение на пустой строке (ожидается ValueError).

Задание 5 (повышенной сложности). Сериализация и сравнение через миксины

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

Требования:

  • DictReprMixin — метод to_dict() -> dict, собирающий публичные атрибуты объекта (через vars(self)), и __repr__ на его основе.
  • JsonMixin — метод to_json() -> str, использующий to_dict() (укажите в требованиях зависимость от DictReprMixin).
  • ComparableMixin — реализует __eq__ и __lt__ на основе кортежа полей, заданного атрибутом класса _compare_fields: tuple[str, ...].
  • Класс Product(DictReprMixin, JsonMixin, ComparableMixin) с полями name, price, _compare_fields = ("price", "name").
  • Покажите сортировку списка Product и сериализацию в JSON; выведите __mro__ и поясните, почему конфликтов имён между миксинами нет.

Критерии оценки

  • Задание 1 (миксины-способности) — 15%.
  • Задание 2 (разбор MRO и ромб, ручной C3) — 25%.
  • Задание 3 (предсказание порядка вызовов super()) — 25%.
  • Задание 4 (кооперативный конвейер миксинов) — 20%.
  • Задание 5 (сериализация/сравнение, повышенная сложность) — 15%.

Дополнительные требования (учитываются в пределах каждого пункта):

  • корректные прогнозы записаны до запуска кода — обязательное условие для заданий 2 и 3;
  • аннотации типов и осмысленные __repr__;
  • кооперативность: единственный вызов super() там, где он нужен;
  • наличие коротких автотестов pytest — до +10% сверх базовой оценки.

Снижение оценки: прямые вызовы методов по имени родителя вместо super() в кооперативных цепочках; отсутствие пояснений к MRO; миксины, создаваемые как самостоятельные экземпляры.


Вопросы для самопроверки

  1. Чем Cls.__mro__ отличается от Cls.mro() и что всегда стоит в конце списка?
  2. Почему для class D(B, C) метод общего предка A выполняется ровно один раз?
  3. Сформулируйте три свойства, которые гарантирует C3-линеаризация.
  4. Вычислите вручную MRO для class A, class B(A), class C(A), class E(C, B). Чем результат отличается от D(B, C)?
  5. Почему говорят, что super() обращается к «следующему классу по MRO», а не к «родителю»? Приведите пример, где это принципиально.
  6. Зачем в кооперативном __init__ использовать **kwargs и передавать их дальше по цепочке?
  7. Что произойдёт, если один из классов в цепочке вызовет Base.__init__(self) напрямую вместо super().__init__()?
  8. Перечислите правила оформления миксинов. Почему их ставят слева от основного базового класса?
  9. В каком случае Python возбудит TypeError при определении класса с множественным наследованием?
  10. Когда стоит предпочесть композицию множественному наследованию?