В чем преимущество композиции перед наследованием?
Что проверяют
Умеете ли вы проектировать слабосвязанные классы и понимаете ли, когда наследование в Python всё-таки уместно. Следом обычно просят пример из кода или спрашивают, как описать контракт компонента, если интерфейсов в языке нет.
Как отвечать
Композиция: объект получает поведение через другие объекты, которые хранит в атрибутах, обычно получая их в __init__. Наследование: поведение приходит от родителя, и потомок связан с его внутренностями. Преимущества композиции:
- замена на лету:
Notifier(sender=SmtpSender())переключается наTelegramSender()без новых подклассов; - нет комбинаторного взрыва: вместо
CachedRetryingJsonClientклиент собирается из кеша, политики повторов и сериализатора; - тестируемость: в тест передаётся фейк, и не нужен
unittest.mock.patchпо строковому пути; - узкая связь: зависимость видна только через публичные методы, правка родителя не ломает потомков (проблема хрупкого базового класса);
- без головоломок с MRO: глубокие иерархии с миксинами требуют держать в голове линеаризацию C3 и кооперативный
super().
Контракт компонента в Python описывают через typing.Protocol (структурная типизация, проверяется mypy или pyright) или через abc.ABC. Часто компонент это просто функция: стратегию сортировки или повторов передают как callable без всякого класса.
Примеры из экосистемы: logging.Logger собирается из обработчиков, форматтеров и фильтров, requests.Session монтирует HTTPAdapter на префиксы адресов.
И назовите, где наследование к месту: настоящее отношение «является», иерархии исключений, фреймворки, которые ждут подкласс (models.Model в Django, TestCase), небольшие миксины без состояния.
Где спотыкаются
Отвечают догмой «наследование плохо». Сильный ответ называет условия, при которых выбирается каждое.
Переносят аргументы из Java: «нет множественного наследования, значит интерфейсы». В Python множественное наследование есть, ромб разрешается через MRO, а роль интерфейсов играют протоколы и ABC.
Делегируют через __getattr__ всё подряд. Код становится непрозрачным для IDE и тайпчекера, а ошибки в именах атрибутов всплывают только во время выполнения.