【问题标题】:Method Resolution Order (MRO) in new-style classes?新型类中的方法解析顺序(MRO)?
【发布时间】:2010-12-23 08:07:05
【问题描述】:

Python in a Nutshell (2nd Edition)一书中有一个例子使用
旧样式类,用于演示如何以经典解析顺序解析方法和
新订单有什么不同。

我通过用新样式重写示例来尝试相同的示例,但结果与使用旧样式类获得的结果没有什么不同。我用来运行示例的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。

【问题讨论】:

    标签: python method-resolution-order


    【解决方案1】:

    当同一个祖先类在“朴素”、深度优先的方法中多次出现时,遗留类与新式类的解析顺序之间的关键区别就出现了——例如,考虑“钻石继承”的情况:

    >>> class A: x = 'a'
    ... 
    >>> class B(A): pass
    ... 
    >>> class C(A): x = 'c'
    ... 
    >>> class D(B, C): pass
    ... 
    >>> D.x
    'a'
    

    这里,legacy-style,解析顺序是 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 的覆盖)实际上可以正常工作。

    这是应避免使用旧样式类的原因之一:具有“类菱形”模式的多重继承对它们不起作用,而对新样式却适用。

    【讨论】:

    • “[祖先类] A [被] 强制在其所有子类之后仅按解析顺序出现一次,因此覆盖(即 C 对成员 x 的覆盖)实际上是合理的。” ——顿悟!多亏了这句话,我又可以在脑海里做MRO了。 \o/ 非常感谢。
    【解决方案2】:

    Python 的方法解析顺序实际上比仅仅理解菱形图案更复杂。要真正了解它,请查看C3 linearization。我发现在扩展方法来跟踪订单时使用打印语句真的很有帮助。例如,您认为这种模式的输出会是什么? (注意:“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()
    

    【讨论】:

    • 你应该更正你的第二个代码:你已经把类“I”作为第一行并且还使用了 super 所以它找到了超类“G”但是“I”是第一类所以它永远不会找到 "G" 类,因为没有 "G" 上 "I" 。将“I”类放在“G”和“F”之间:)
    • 示例代码不正确。 super 具有必需的参数。
    • 在类定义中 super() 不需要参数。见https://docs.python.org/3/library/functions.html#super
    • 你的图论太复杂了。在第 1 步之后,将左侧类的边插入到右侧的类(在任何继承列表中),然后执行 topological sort 即可。
    • @Kevin 我不认为这是正确的。按照我的例子, A C D B E F G H 不是有效的拓扑排序吗?但这不是解决顺序。
    【解决方案3】:

    你得到的结果是正确的。尝试将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
    

    阅读this explanation了解更多信息。

    【讨论】:

      【解决方案4】:

      您会看到这种行为,因为方法解析是深度优先的,而不是广度优先的。 Dervied 的继承看起来像

               Base2 -> Base1
              /
      Derived - Base3
      

      所以instance.amethod()

      1. 检查 Base2,没有找到方法。
      2. 发现 Base2 继承自 Base1,并检查 Base1。 Base1 有一个amethod,所以它被调用了。

      这反映在Derived.__mro__。只需遍历 Derived.__mro__ 并在找到正在查找的方法时停止。

      【讨论】:

      • 我怀疑我得到“Base1”作为答案的原因是因为方法分辨率是深度优先的,我认为它比深度优先方法更多。请参阅 Denis 的示例,如果是深度优先 o/p 应该是“Base1”。另请参阅您提供的链接中的第一个示例,其中显示的 MRO 还表明方法分辨率不仅仅是通过深度优先顺序遍历来确定的。
      • 抱歉,MRO 上的文档链接由 Denis 提供。请检查一下,我误以为您向我提供了 python.org 的链接。
      • 它通常是深度优先的,但正如 Alex 解释的那样,处理类似钻石的继承是有技巧的。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-09-10
      • 2021-02-20
      • 2016-09-10
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多