Python 的菱形继承和 MRO

Published 7/17/2020
Modified 7/19/2020
Views 46

Python 是一门(在我看来很不幸地)支持多继承的语言。既然支持了多继承,那难以避免的一个问题就是菱形继承。考虑下图的类关系:

继承图

对应代码:

class Base:
    pass

class Father(Base):
    def __init__(self):
        super().__init__()

class Mother(Base):
    def __init__(self):
        super().__init__()

class Child(Father, Mother):
    def __init__(self):
        super().__init__()

当我们实例化一个 Child(),它会怎么调用其父类的初始化函数呢?或者说,super().__init__() 这个调用过程中,实际发生了什么?

1. super() 不是父类

首先需要明确的是,调用 super()得到的 并不是父类对象

如果 super() 只是简单地将调用委托给其全部的父类,那么我们就会得到这样的调用:

Child -> Father -> Base --> Mother -> Base

注意,这里不可避免地产生了两次 Base 调用,这种处理方法显然是存在问题的。

2. super() 和 MRO

python 的文档很清楚地指出, super()得到的是 一个代理对象,将调用委托给type的父类或兄弟类

在我们上面的例子中,Childsuper() 得到的是一个 Father 对象的代理,因此在 Child.__init__ 上下文中调用 super().__init__() 实际相当于 Father.__init__(self)

同样, Father 中的 super() 得到的是 Mother 对象的代理(而不是 Base),Mother 中的 super() 才是 Base 对象的代理。

调用链条为

Child -> Father -> Mother -> Base (-> object)

这个顺序被称为 python 的 Method Resolution Order (MRO)。MRO 保证了在合法的继承中,每个类只会被遍历一次。因此,super() 委托的类就是 MRO 调用链中的下一个类。

在 python 中,可以使用 Class.mro() 获取类的 MRO 链。

3. MRO 的要求和实现

知道了 super() 的实现,我们进一步想知道 MRO 的要求和其在 python 中的实现。

考虑一个继承关系的集合,其应该构成一个有向无环图(我不应该继承我自己)。因此,一个合法的 MRO 应该是一个有向无环图的遍历。

对于继承关系,我们还需要一些要求:

  • MRO 应该是接近 BFS 的,因此可以从更接近最终类的地方查找实现。如果采用 DFS,可能查找到更 abstract 的实现。

  • MRO 应该是 单调的。这里,单调定义为:

    • 如果在类 C 的线性化过程中,C1 在 C2 类之前,那么在类 C 的所有子类中,C1 都应该在 C2 类之前。

考虑这样的一个复杂继承关系:

假设在类 A 的线性化中,X 出现在 Y 之前(class A(X, Y));而在类 B 的线性化中,Y 出现在 X 之前(class B(Y, X))。这样一来,如果 C 继承于 A、B,那么类 C 就无法找到一个合法的 MRO 满足单调性。

稍微修改一下类 B 为 class B(X, Y),就可以算出一个满足要求的 MRO:

C -> A -> B -> X -> Y -> O

python 使用 C3 算法 来实现 MRO。其详细算法可以参见参考资料[1]。博文由于篇幅不再继续展开。


Refer:

[1] https://www.python.org/download/releases/2.3/mro/

0 comments