Карцигеникат уже дал очень хороший ответ. Я просто хотел добавить кое-что об этой части:
Я изо всех сил пытаюсь понять, почему (композиция) предпочтительнее наследования.
На самом деле, речь идет о композиции / делегировании, не только композиция (которая сама по себе не обеспечивает те же функции). Дело в том, что наследование на самом деле служит двум целям: подтипу и повторному использованию реализации.
Подтип обозначает отношение «является» - если B является (правильным) подтипом A, вы можете использовать B везде, где можете использовать A. На самом деле Принцип замещения Лискова выражает это наоборот: «B является правильным подтипом A, если любой код, принимающий A, может вместо этого принять B ". Обратите внимание, что это ничего не говорит о наследовании и реализации, и что ни один из них не требуется (на теоретическом уровне) для подтипирования.
Теперь со статически типизированными языками у вас есть использовать наследование для подтипа, точка - и вы в конечном итоге получите повторное использование реализации в качестве бонуса, если есть какая-либо реализация, чтобы хотя бы повторно использовать.
OTHO Python для динамической типизации не требуется наследование для подтипирования (если, конечно, код, использующий ваш объект, не выполняет никакой проверки типов) - достаточно иметь совместимый интерфейс. Таким образом, в Python наследование главным образом связано с повторным использованием реализации.
Теперь, технически, повторное использование реализации через наследование является формой композиции / делегирования - ваш объект является экземпляром своего собственного класса, но также и всех его суперклассов, и любой атрибут не разрешен ни в вашем экземпляре, ни в этот класс будет проверен на родительских классах. Основное отличие от «ручного» составления / делегирования заключается в том, что наследование более ограничено - вы не можете изменить, кому вы делегируете, например, во время выполнения или для каждого экземпляра (ну ... в Python, вы на самом деле может , технически, изменить класс экземпляра во время выполнения, но на практике это очень плохая идея и никогда не работает как положено - здесь, делали это xD).
Таким образом, при повторном использовании / реализации реализация наследования является ограниченной и в основном статической c формой составления / делегирования. Это хорошо для довольно многих случаев использования - обычно для создания более или менее абстрактных базовых классов, которые должны быть унаследованы, et c, - но некоторые другие случаи лучше решать с помощью более динамичного c решения (канонические примеры являются шаблонами разработки состояния и стратегии, но есть множество других).
Кроме того, даже если используется только для повторного использования реализации, наследование STILL подразумевает отношение «является» - даже если дочерний класс не является надлежащим подтипом своей базы (любая несовместимость нарушит правильный подтип, и ничто не мешает чтобы вы изменили некоторые из ваших методов подкласса с несовместимыми сигнатурами) - и Python не имея никакого понятия «частное наследование», ваш дочерний класс будет раскрывать весь свой унаследованный интерфейс, что не обязательно то, что вы хотите (и на самом деле довольно часто что вы не хотите, когда просто делаете реализацию повторно). И, конечно, если вы решите изменить, какую реализацию вы хотите использовать повторно, тогда ... наследование вводит гораздо более сильную связь (и это занижение), чем составление / делегирование.
Типичный пример "ошибки новичка" наследовать от некоторого встроенного типа коллекции (скажем, list
, например) и пытаться "ограничить" его конкретными потребностями c. Это никогда не работает должным образом и обычно требует гораздо больше работы, чем использование композиции / делегирования. Затем они понимают, что (по-прежнему, например) OrderedDict был бы гораздо лучшей основой для их собственного варианта использования, а затем у них возникает проблема с тем, что клиентский код теперь зависит от унаследованного list
интерфейса ... Использование композиции / делегирования с самого начала предотвратило бы много боли - оставив интерфейс ограниченным соответствующими функциями вместо утечки унаследованного интерфейса и сохранив элемент «повторного использования реализации» таким, какой он есть на самом деле: деталь реализации о том, что клиентский код никогда не должен был знать.
Основная проблема на самом деле заключается в очень многих очень плохих текстах «OO 101», которые представляют наследование как одну из ключевых функций OO (а это не так - настоящая «Ключевыми OO-функциями» являются инкапсуляция - не путать с сокрытием данных BTW - и диспетчеризация на основе типов polymorphi c), что приводит к тому, что новички пытаются злоупотреблять наследованием, даже не осознавая, что существуют другие - а иногда и гораздо лучшие - решения.
Короче говоря: как и любое другое «золотое правило», отдавать предпочтение композиции / делегированию, а не наследованию, - это только «правило», когда вы не понимаете плюсы и минусы каждого решения и когда каждое из них более уместно - IOW, как и любое «золотое правило», вы НЕ хотите слепо принимать и применять его (что привело бы к глупому выбору дизайна), но - как вы правильно сделали - ставить его под сомнение, пока не поймете, о чем оно на самом деле и даже не нужно больше об этом думать.
О, да: вы можете узнать о __getattr__
волхвах c методе (для делегирования) - и протокол дескриптора и встроенный тип property
(для поддержки вычисляемых атрибутов), пока вы в нем (подсказка: вам не нужны эти приватные атрибуты / publi c средства доступа в Python) .