新式类的方式解析顺序(MRO)?

111 投票
4 回答
61053 浏览
提问于 2025-04-15 16:42

在书籍《Python in a Nutshell (第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 个回答

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

想了解更多信息,可以看看 这个解释

28

Python的方法解析顺序其实比单纯理解菱形模式要复杂得多。要想真正理解这个问题,可以看看C3线性化。我发现,在扩展方法时使用打印语句来跟踪顺序非常有帮助。例如,你觉得这个模式的输出会是什么?(注意:'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()
205

老式类和新式类在解析顺序上的一个关键区别出现在同一个祖先类出现多次的时候,尤其是在“钻石继承”的情况下。

>>> 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:所以当我们查找 D.x 时,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 的重写)才能正常工作。

这也是为什么应该避免使用老式类的原因之一:在“钻石型”模式下的多重继承在老式类中并不能正常工作,而在新式类中却可以。

撰写回答