Порядок разрешения методов (MRO) в классах нового стиля?
В книге " Питон в двух словах" (2-е издание) есть пример, который использует
классы старого стиля, чтобы продемонстрировать, как методы разрешаются в классическом порядке разрешения и
как это отличается с новым заказом.
Я попробовал тот же пример, переписав пример в новом стиле, но результат ничем не отличается от того, который был получен с классами старого стиля. Версия Python, которую я использую для запуска примера, - 2.5.2. Ниже приведен пример:
class Base1(object):
def amethod(self): print "Base1"
class Base2(Base1):
pass
class Base3(object):
def amethod(self): print "Base3"
class Derived(Base2,Base3):
pass
instance = Derived()
instance.amethod()
print Derived.__mro__
Вызов instance.amethod()
печать Base1
, но согласно моему пониманию MRO с новым стилем классов, вывод должен был быть Base3
, Вызов Derived.__mro__
печатает:
(<class '__main__.Derived'>, <class '__main__.Base2'>, <class '__main__.Base1'>, <class '__main__.Base3'>, <type 'object'>)
Я не уверен, что мое понимание MRO с новыми классами стилей неверно или я делаю глупую ошибку, которую не могу обнаружить. Пожалуйста, помогите мне в лучшем понимании MRO.
4 ответа
Принципиальное различие между порядком разрешения для устаревших классов и классов нового стиля возникает, когда один и тот же класс предков встречается более одного раза в "наивном" подходе, основанном на глубине - например, рассмотрим случай "наследования алмазов":
>>> class A: x = 'a'
...
>>> class B(A): pass
...
>>> class C(A): x = 'c'
...
>>> class D(B, C): pass
...
>>> D.x
'a'
здесь, в традиционном стиле, порядок разрешения - D - B - A - C - A: поэтому при поиске Dx A является первой базой в разрешении, чтобы решить ее, тем самым скрывая определение в C. В то время как:
>>> class A(object): x = 'a'
...
>>> class B(A): pass
...
>>> class C(A): x = 'c'
...
>>> class D(B, C): pass
...
>>> D.x
'c'
>>>
здесь, в новом стиле, порядок:
>>> D.__mro__
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>,
<class '__main__.A'>, <type 'object'>)
с A
вынужден входить в порядок разрешения только один раз и после всех своих подклассов, так что переопределяет (то есть переопределение C члена x
) на самом деле работает разумно.
Это одна из причин, по которой следует избегать классов в старом стиле: множественное наследование с "ромбовидными" шаблонами просто не работает с ними разумно, в то время как это происходит с новым стилем.
Порядок разрешения методов в Python на самом деле сложнее, чем просто понимание структуры ромба. Чтобы действительно понять это, взгляните на линеаризацию C3. Я обнаружил, что действительно полезно использовать операторы print при расширении методов для отслеживания порядка. Например, как вы думаете, что будет выход из этого шаблона? (Примечание: предполагается, что "X" - это два пересекающихся ребра, а не узел, и ^ означает методы, которые вызывают super())
class G():
def m(self):
print("G")
class F(G):
def m(self):
print("F")
super().m()
class E(G):
def m(self):
print("E")
super().m()
class D(G):
def m(self):
print("D")
super().m()
class C(E):
def m(self):
print("C")
super().m()
class B(D, E, F):
def m(self):
print("B")
super().m()
class A(B, C):
def m(self):
print("A")
super().m()
# A^
# / \
# B^ C^
# /| X
# D^ E^ F^
# \ | /
# G
Вы получили A B D C E F G?
x = A()
x.m()
После многих проб и ошибок я придумал неформальную интерпретацию теории линеаризации C3 в теории графов следующим образом: (Кто-то, пожалуйста, дайте мне знать, если это не так.)
Рассмотрим этот пример:
class I(G):
def m(self):
print("I")
super().m()
class H():
def m(self):
print("H")
class G(H):
def m(self):
print("G")
super().m()
class F(H):
def m(self):
print("F")
super().m()
class E(H):
def m(self):
print("E")
super().m()
class D(F):
def m(self):
print("D")
super().m()
class C(E, F, G):
def m(self):
print("C")
super().m()
class B():
def m(self):
print("B")
super().m()
class A(B, C, D):
def m(self):
print("A")
super().m()
# Algorithm:
# 1. Build an inheritance graph such that the children point at the parents (you'll have to imagine the arrows are there) and
# keeping the correct left to right order. (I've marked methods that call super with ^)
# A^
# / | \
# / | \
# B^ C^ D^ I^
# / | \ / /
# / | X /
# / |/ \ /
# E^ F^ G^
# \ | /
# \ | /
# H
# (In this example, A is a child of B, so imagine an edge going FROM A TO B)
# 2. Remove all classes that aren't eventually inherited by A
# A^
# / | \
# / | \
# B^ C^ D^
# / | \ /
# / | X
# / |/ \
# E^ F^ G^
# \ | /
# \ | /
# H
# 3. For each level of the graph from bottom to top
# For each node in the level from right to left
# Remove all of the edges coming into the node except for the right-most one
# Remove all of the edges going out of the node except for the left-most one
# Level {H}
#
# A^
# / | \
# / | \
# B^ C^ D^
# / | \ /
# / | X
# / |/ \
# E^ F^ G^
# |
# |
# H
# Level {G F E}
#
# A^
# / | \
# / | \
# B^ C^ D^
# | \ /
# | X
# | | \
# E^F^ G^
# |
# |
# H
# Level {D C B}
#
# A^
# /| \
# / | \
# B^ C^ D^
# | |
# | |
# | |
# E^ F^ G^
# |
# |
# H
# Level {A}
#
# A^
# |
# |
# B^ C^ D^
# | |
# | |
# | |
# E^ F^ G^
# |
# |
# H
# The resolution order can now be determined by reading from top to bottom, left to right. A B C E D F G H
x = A()
x.m()
Результат вы получите правильно. Попробуйте изменить базовый класс Base3
в Base1
и сравните с той же иерархией для классических классов:
class Base1(object):
def amethod(self): print "Base1"
class Base2(Base1):
pass
class Base3(Base1):
def amethod(self): print "Base3"
class Derived(Base2,Base3):
pass
instance = Derived()
instance.amethod()
class Base1:
def amethod(self): print "Base1"
class Base2(Base1):
pass
class Base3(Base1):
def amethod(self): print "Base3"
class Derived(Base2,Base3):
pass
instance = Derived()
instance.amethod()
Теперь он выводит:
Base3
Base1
Прочитайте это объяснение для получения дополнительной информации.
Вы видите такое поведение, потому что разрешение метода - сначала глубина, а не ширина. Наследство дервида выглядит
Base2 -> Base1
/
Derived - Base3
Так instance.amethod()
- Проверяет Base2, не находит метод.
- Видит, что Base2 унаследовал от Base1, и проверяет Base1. Base1 имеет
amethod
так оно и называется.
Это отражено в Derived.__mro__
, Просто перебрать Derived.__mro__
и остановитесь, когда найдете метод, который искали.