Сообщить об ошибке.

Множественное наследование классов

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'>]

Не все классы поддаются линеаризации алгоритмом MRO.

Допустим, есть иерархия классов, которая представлена выше в примере. В ней, нет ничего криминального, но если попробовать наследоваться от классов 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 не проводит никаких дополнительных проверок и можно спокойно провести поиск в предках раньше чем в потомках. И хотя это не желательная практика, но это возможно.

Осложнения в множественном наследовании 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.