CoderXL's Blog

Back

技术笔记与分享 →

诡异的多继承机制#

Python 是面向对象的语义,有健全的类与继承设计。这也意味着它必须直面继承中最让语言们头疼的一个机制——菱形继承。

例如,

class A():
    def play(self):
        print('A')

class B(A):
    def play(self):
        print('B')

class C(A):
    def play(self):
        print('C')

class D(B,C):
    pass

D().play()
python

请问结果是什么?(运行试试)


如果你只看 Case #2 和 Case #3 的 B 的定义,会感觉二者的效果是完全一样的。毕竟 B 继承了 A,B.play 自然就是 A.play,显式地写一遍纯属多余,对吗?对…对对吗?

发现 Case #2 和 Case #3 的输出竟然不一样!

这就涉及到 Python 的一个隐藏特性:MRO(Method Resolution Order)。


Case #1#

想象一下,小 D 游戏瘾又犯了,想开一局三角洲,可惜自己的手机被老师没收了,只能找家里人借。还好他们家人多。

于是小 D 盘算好,先找哥哥 B 借,不行的话再问姐姐 C,但是绝不能直接问爸爸 A,不然代价有点大(自行脑补)。

于是他先找 B 问,正好 B 同意了,于是小 D 成功上号,用的是他哥小 B 的账号。

Case #2#

但是这次哥哥 B 的手机也被老师没收了,B 自己想玩也只能找爸爸借。

当小 D 再次找到 B 的时候,B 跟他说没办法,要玩手机的话也可以,我帮你找爸爸借。

小 D 犹豫了,因为他不想轻易惊动爸爸,何况还可以再找姐姐 C 问,C 也没有还可以让她去出面跟爸说。

姐姐 C 把手机给他玩了。

Case #3#

但是这次哥哥 B 的手机虽然没被没收,但是自己的号防沉迷了,于是登了爸爸的号。这次小 D 借过来上号了,登的是爸爸的号。


上面的关键点在于,哥哥 B 到底是自己有手机但是登了爸爸的号,还是自己没手机要找爸爸借。

例子看完了,来进入一下真正的 MRO.

MRO#

在 Python 中,继承语句 D(B,C) 相当于声明了 D 的一个方法或者属性的查找顺序:D > B > C. 结合 B 与 C 的继承,可以写出另外两个顺序:B > A 和 C > A.

目前这是一个偏序关系,但是可以赋予一个全序:D > B > C > A,不难发现所有不等式都是满足的。

在这种定义下,全序是唯一的。

但如果把 class C(A) 改成 class C,不再继承自 A(也就是删去 C > A 这个不等式),那么全序就有两个解:

  • D > B > C > A
  • D > B > A > C

而 Python 就是按照这个全序去查找方法和属性的。简单来说,就是要保证父类的属性查找一定在其所有子类之后。请注意,这些顺序仅作用于 D 及其实例,B、C 的顺序与此没有必然联系。

而 Python 用于从偏序计算全序的规范,叫做 Python C3.


Key Takeaway#

下次再看到一个大项目里写着

class B(A):
    func = A.func
python

的时候,千万别再把这句当成废话给优化掉了。虽说对于 B 本身而言,删掉这句话效果没变化,但是可能会导致 B 的子类出现一些隐藏 bug.

Comment seems to stuck. Try to refresh?✨