Порядок разрешения методов (MRO) в классах нового стиля? - PullRequest
84 голосов
/ 04 декабря 2009

В книге 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.

Ответы [ 4 ]

163 голосов
/ 04 декабря 2009

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

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

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

21 голосов
/ 27 декабря 2014

Порядок разрешения методов 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()
5 голосов
/ 04 декабря 2009

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

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

0 голосов
/ 04 декабря 2009

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

         Base2 -> Base1
        /
Derived - Base3

То есть instance.amethod()

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

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

...