Порядок разрешения методов (MRO) в классах нового стиля?

99

В книге « Python в двух словах» (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.

сатиш
источник

Ответы:

186

Ключевое различие между порядком разрешения для устаревших классов и классов нового стиля возникает, когда один и тот же класс-предок встречается более одного раза в «наивном» подходе с ориентацией в глубину - например, рассмотрим случай «наследования ромба»:

>>> 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) действительно работают разумно.

Это одна из причин, по которой следует избегать классов старого стиля: множественное наследование с «ромбовидными» шаблонами просто не работает с ними разумно, в то время как с новым стилем.

Алекс Мартелли
источник
2
«[класс-предок] A [вынужден] идти в порядке разрешения только один раз и после всех его подклассов, так что переопределения (то есть переопределение C члена x) действительно работают разумно». - Крещение! Благодаря этому предложению я снова могу мысленно проводить ТОиР. \ o / Большое спасибо.
Эстейс,
25

Порядок разрешения методов Python на самом деле более сложен, чем просто понимание ромбовидного узора. Чтобы действительно понять это, взгляните на С3 линеаризацию . Я обнаружил, что использование операторов печати действительно помогает при расширении методов для отслеживания порядка. Например, как вы думаете, каким будет результат этого шаблона? (Примечание: '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

Вы получили ABDCEFG?

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()
Бен
источник
Вы должны исправить свой второй код: вы поместили класс «I» в качестве первой строки, а также использовали super, чтобы он находил суперкласс «G», но «I» является первым классом, поэтому он никогда не сможет найти класс «G», потому что там нет "G" верхнего "I". Поместите класс «I» между «G» и «F» :)
Адитья Ура 07
Код примера неверен. superимеет необходимые аргументы.
Danny
2
Внутри определения класса super () не требует аргументов. См. Https://docs.python.org/3/library/functions.html#super
Бен,
Ваша теория графов излишне сложна. После шага 1 вставьте ребра из классов слева в классы справа (в любом списке наследования), а затем выполните топологическую сортировку, и все готово.
Кевин
@Kevin Я не думаю, что это правильно. Следуя моему примеру, не будет ли ACDBEFGH допустимой топологической сортировкой? Но это не порядок разрешения.
Бен
5

Полученный результат правильный. Попробуйте изменить базовый класс 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

Прочтите это объяснение для получения дополнительной информации.

Денис Откидач
источник
1

Вы наблюдаете такое поведение, потому что разрешение метода ориентировано на глубину, а не в ширину. Наследование Dervied выглядит как

         Base2 -> Base1
        /
Derived - Base3

Так instance.amethod()

  1. Проверяет Base2, не находит метода.
  2. Видит, что Base2 унаследован от Base1, и проверяет Base1. У Base1 есть amethod, поэтому он вызывается .

Это отражено в Derived.__mro__. Просто повторите Derived.__mro__и остановитесь, когда найдете искомый метод.

Jamessan
источник
Я сомневаюсь, что причина, по которой я получаю «Base1» в качестве ответа, заключается в том, что разрешение метода ориентировано на глубину, я думаю, что это больше, чем подход, ориентированный на глубину. См. Пример Дениса: если бы это была глубина, то o / p должно было быть "Base1". Также обратитесь к первому примеру в предоставленной вами ссылке, там также показано, что MRO показывает, что разрешение метода не просто определяется обходом в порядке глубины.
сатиш 04
Извините, ссылка на документ по ТОиР предоставлена ​​Денисом. Пожалуйста, проверьте, я ошибся, что вы предоставили мне ссылку на python.org.
сатиш 04
4
Как правило, он ориентирован на глубину, но, как объяснил Алекс, есть умения для обработки алмазоподобного наследования.
jamessan