MRO 多继承

MRO, Method Resolution Order

class A:
    def say(self):
        print("A")

class M(A):
    pass

m = M()
m.say()

输出为:A

class A:
    def say(self):
        print("A")

class B:
    def say(self):
        print("B")

class M(B):
    pass

m = M()
m.say()

输出为:B

class A:
    def say(self):
        print("A")

class B:
    def say(self):
        print("B")

class M(A, B):
    pass

m = M()
m.say()

输出为:A

class A:
    def say(self):
        print("A")

class B:
    def say(self):
        print("B")

class C(A):
    pass

class M(C, B):
    pass

m = M()
m.say()

输出为:A

class A:
    def say(self):
        print("A")

class B(A):
    def say(self):
        print("B")

class C(A):
    pass

class M(C, B):
    pass

m = M()
m.say()

输出为:B

怎么从父类里找某个类的优先函数?这就是MRO

两种方式:

  1. M.mro()
  2. M.__mro__

上面最后一个例子打印出来像这样:

[__main__.M, __main__.C, __main__.B, __main__.A, object]

可以看出,在调用M的say()方法时,先在M中找没找到,又在C中找,没找到,最后在B中找,找到了,用的就是B中的say()方法,优先级越来越低

算法

python从2.3版本用的就是C3 MROwiki | paper

C3算法的三个C:(阅读顺序:2->3->1,标号只为了与维基百科一致)

  1. a consistent extended precedence graph:把每对类都画上箭头,箭头方向取决于“在这对类的最小公共子类上,这两个类或它们的子类的优先级顺序”(比较难理解,看下面这幅图的解释)

    precedence graph类似于下图,指子类和父类之间的关系

    image-20220417175154541

    解释:观察scrolling-mixinediting-mixin两个类,第二个C决定了所有scrollable-pane及其子类中,pane都优先于scrolling-mixin,同理所有editable-pane及其子类中,pane都优先于editing-mixin。第三个C保证了在editable-scrollable-pane中,使用的方法一定scrollable-paneeditable-pane中使用的方法。第一个C确定了scrolling-mixinediting-mixin的顺序:

    • 找到两者的最小公共子类,即保证这个子类的所有父类都不是两者共同的子类,这个例子中就是editable-scrollable-pane
    • 只要scrolling-mixinscrolling-mixin的子类排在editing-mixinediting-mixin的子类之前,则scrolling-mixin优先级就高于editing-mixin,这个例子中,由于scrollable-pane排在editable-pane之前,所以scrolling-mixin优先级高于editing-mixin
  2. preservation of local precedence order(局部的优先顺序):当一个class继承了多个class时,会优先使用写在前面的class中的方法;同时,这个class的所有subclass(子类)也同样保持这个特性。比如M继承了(A,B)(注意顺序),C继承了M,对C调用say()方法的时候A的优先级也比B高

    比如:

    class A:
        def say(self):
            print("A")
    
    class B:
        def say(self):
            print("B")
    
    class M(A, B):
        pass
    
    m = M()
    m.say()
    

    输出A,然而:

    class A:
        def say(self):
            print("A")
    
    class B:
        def say(self):
            print("B")
    
    class M(B, A):
        pass
    
    m = M()
    m.say()
    

    输出B

  3. fitting a monotonicity criterion(符合单调性标准):任何一个class使用的方法必须来自其直接父类使用的方法,这一部分比较复杂,大家可以去b站看码农高天的视频,很清晰的。这是论文中提到的一个例子

    image-20220417174616934

    假设有一个方法只存在于day-boat和wheel-boat上,可以看到,pedalo的两个父类:pedal-wheel-boat和small-catamaran在继承时优先使用day-boat的方法(由它们的MRO得来),而pedalo作为它们的子类,优先使用的是wheel-boat的方法,它没有使用父类使用的方法,或者说它跳过了父类,使用了其他类的方法,这就破坏了单调性标准

不满足3C算法的类定义

class A:
    def say(self):
        print("A")
        
class B:
    def say(self):
        print("B")

# 由第二个C知道,对class C来说,A在B前面
class C(A, B):
    pass

# 由第二个C知道,对class D来说,B在A前面
class D(B, A):
    pass

# 由第一个C知道,由于C排在D之前,那说明A应该排在B之前
# 但class D规定B排在A前面,产生矛盾
class M(C, D):
    pass

继承关系如下图所示:

image-20220417182916334

结语

多继承在python中是被允许的语法,在java中则取消了多继承。MRO最基本需要掌握在写完代码之后怎么通过mro()方法或__mro__成员判断调用方法时候的优先级。文中提到的up主码农高天在讲解python一些比较细致和深入的问题上很清晰,感兴趣的小伙伴可以关注关注。如果有任何疑问或错误欢迎在评论区里提出,我们一起讨论~