Практика 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). - В отдельном комментарии вручную примените правило
mergeC3-линеаризации для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; миксины, создаваемые как
самостоятельные экземпляры.
Вопросы для самопроверки
- Чем
Cls.__mro__отличается отCls.mro()и что всегда стоит в конце списка? - Почему для
class D(B, C)метод общего предкаAвыполняется ровно один раз? - Сформулируйте три свойства, которые гарантирует C3-линеаризация.
- Вычислите вручную MRO для
class A,class B(A),class C(A),class E(C, B). Чем результат отличается отD(B, C)? - Почему говорят, что
super()обращается к «следующему классу по MRO», а не к «родителю»? Приведите пример, где это принципиально. - Зачем в кооперативном
__init__использовать**kwargsи передавать их дальше по цепочке? - Что произойдёт, если один из классов в цепочке вызовет
Base.__init__(self)напрямую вместоsuper().__init__()? - Перечислите правила оформления миксинов. Почему их ставят слева от основного базового класса?
- В каком случае Python возбудит
TypeErrorпри определении класса с множественным наследованием? - Когда стоит предпочесть композицию множественному наследованию?