Python также поддерживает форму множественного наследования. Определение класса с несколькими базовыми классами выглядит следующим образом:
class DerivedClassName(Base1, Base2, Base3): <statement-1> . <statement-N>
Для большинства целей, в простейших случаях, можно думать о поиске атрибутов, унаследованных от родительского класса, как о поиске в глубину слева направо, а не о поиске дважды в одном и том же классе, где есть перекрытие в иерархии. Таким образом, если атрибут не найден в DerivedClassName
, он ищется в Base1
, затем рекурсивно в базовых классах класса Base1
, и если он там не был найден, он ищется в Base2
и так далее.
На самом деле, это несколько сложнее. Порядок разрешения метода (MRO - method resolution order) динамически изменяется для поддержки совместных вызовов super()
. Этот подход известен в некоторых других языках множественного наследования как call-next-method
и является более мощным, чем супер-вызов, встречающийся в языках единственного наследования.
Динамическое упорядочение необходимо, так как во всех случаях множественного наследования присутствует одно или несколько ромб-отношений, где по крайней мере, один из родительских классов может быть доступен по нескольким путям из самого нижнего класса. Например, все классы наследуются от объекта object
, поэтому любой случай множественного наследования предоставляет более одного пути для достижения объекта object
.
Чтобы предотвратить повторный доступ к базовым классам, динамический алгоритм линеаризует порядок поиска таким образом, чтобы сохранить порядок слева направо, указанный в каждом классе, который вызывает каждого родителя только один раз и является монотонным. Это означает, что класс может быть подклассом, не затрагивая порядок приоритета его родителей. В совокупности эти свойства позволяют создавать надежные и расширяемые классы с множественным наследованием.
Чтобы получить MRO класса, можно использовать, либо атрибут проверяемого класса DerivedClassName.__mro__
, либо его метод DerivedClassName.mro()
.
>>> class X(object): pass ... >>> class Y(object): pass ... >>> class A(X, Y): pass ... >>> class B(Y, X): pass ... >>> A.__mro__ # (<class '__main__.A'>, <class '__main__.X'>, <class '__main__.Y'>, <class 'object'>) >>> B.mro() # [<class '__main__.B'>, <class '__main__.Y'>, <class '__main__.X'>, <class 'object'>]
Допустим, есть иерархия классов, которая представлена выше в примере. В ней, нет ничего криминального, но если попробовать наследоваться от классов A
и B
, то интерпретатор выдаст весьма интересную ошибку:
>>> class C(A, B): pass ... # Traceback (most recent call last): # File "<stdin>", line 1, in <module> # TypeError: Cannot create a consistent method resolution # order (MRO) for bases X, Y
Причиной ошибки послужил алгоритм MRO (method resolution order). Под MRO класса C
понимается его линеаризация - список предков класса, включая сам класс, отсортированный в порядке "удалённости". Для конструирования линеаризации класса в Python используется C3 linearization
алгоритм, который был принят в Python и ещё, например, в Perl 6. Линеаризацией данного класса называется слияние линеаризацией его родителей.
Вот как работает алгоритм линеаризации для определения пути поиска в символической нотации (для примера выше):
L[C] = [C] + merge(L[A], L[B], [A, B]) = = [C] + merge([A, X, Y], [B, Y, X], [A, B]) = = [C] + [A, B] + merge([X, Y], [Y, X])
Как видите, конфликт получился неразрешимый, поскольку в объявлении класса A
базовый класс X
стоит перед базовым классом Y
, а в объявлении класса B
- наоборот. По-хорошему, с этого места надо идти и пересматривать структуру. Но если очень надо быстро подхамячить или вы просто знаете что делаете, то язык Python никогда, никого не ограничивал. Свою собственную линеаризацию можно задать через мета-классы. Для этого достаточно в мета-классе указать метод mro(cls). По сути, переопределить метод базового мета-класса type
, который вернёт нужную вам линеаризацию.
class MetaMRO(type): def mro(cls): return (cls, X, Y, A, B object)
Дальше объявляем класс следующим образом:
>>> class C(A, B, metaclass=MetaMRO): pass ... >>> C.__mro__ # (<class '__main__.C'>, <class '__main__.X'>, <class '__main__.Y'>, # <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)
Обратите внимание, что если вы берёте на себя ответственность за MRO, Python не проводит никаких дополнительных проверок и можно спокойно провести поиск в предках раньше чем в потомках. И хотя это не желательная практика, но это возможно.
Что происходит, когда классы, от которых происходит наследование, имеют общий атрибут? Чье значение принимает дочерний класс? Давайте возьмем три класса А, В и С.
>>> class A: id = 1 ... >>> class B: id = 2 ... >>> class C: id = 3 ... >>> class M(A,C,B): pass ... >>> M.id # 1 >>> class M(C,B,A): pass ... >>> M.id # 3
Как мы видим, класс, названный первым в наследовании, передает свое значение дочернему классу для общего атрибута. То же самое происходит и с методами/функциями класса Python.