「Python 进阶之路」系列 Day13
写在前面
如果一个类同时继承了两个父类,而这两个父类又都继承自同一个祖先类,调用一个三方都定义过的同名方法,到底会执行谁的实现?这是多重继承下最经典的疑问,也是 super() 最容易被误解的地方------很多人以为 super().method() 调用的是"我的父类",但在多重继承场景下,事实往往不是这样。
今天这篇把 Python 用来解决这个问题的 MRO(方法解析顺序)和背后的 C3 线性化算法讲透。
一、是什么:多重继承与方法解析顺序
Python 允许一个类同时继承多个父类(多重继承)。当多个父类都定义了同名方法时,Python 需要一套规则来决定调用顺序------这套规则算出来的顺序就是 MRO(Method Resolution Order,方法解析顺序) ,可以用 ClassName.__mro__ 或 ClassName.mro() 直接查看。
python
class A:
def hello(self):
print("A.hello")
class B(A):
def hello(self):
print("B.hello")
class C(A):
def hello(self):
print("C.hello")
class D(B, C):
pass
d = D()
d.hello() # B.hello ------ 到底为什么是B,不是C,也不是A?
print([c.__name__ for c in D.__mro__])
# ['D', 'B', 'C', 'A', 'object']
二、为什么需要 C3 线性化:钻石继承问题
上面这种 D 同时继承 B、C,而 B、C 又都继承自 A 的结构,就是经典的钻石继承(菱形继承):
如果简单粗暴地用"深度优先"去遍历(先一路找到底:D → B → A,找不到再回头找 C → A),会导致 A 被重复访问,而且顺序还可能违反直觉。Python(从 2.3 版本起)用 C3 线性化算法计算 MRO,保证三条关键性质:
- 子类永远排在父类前面(单调性)
- 一个类继承的多个父类之间,相对顺序按照声明时的顺序保留 (声明
class D(B, C)时 B 在 C 前面,MRO 里 B 也必须排在 C 前面) - 每个类在 MRO 里只出现一次 (
A不管被继承多少次,只会出现一次,且排在所有依赖它的子类之后)
D 的 MRO 结果 ['D', 'B', 'C', 'A', 'object'] 正好体现了这三点:D 最先(子类优先),B 在 C 前面(声明顺序),A 只出现一次且排在 B、C 之后(因为 B、C 都依赖 A)。
三、怎么用
1. super() 调用的是 MRO 里的下一个类,不是父类
这是多重继承里最容易被误解的点:super().method() 调用的不是 "我的父类",而是当前类在 MRO 链条里的下一个类。
python
class X:
def hello(self):
print("X.hello")
class Y(X):
def hello(self):
print("Y.hello, 即将调用super")
super().hello()
class Z(X):
def hello(self):
print("Z.hello, 即将调用super")
super().hello()
class W(Y, Z):
def hello(self):
print("W.hello, 即将调用super")
super().hello()
print([c.__name__ for c in W.__mro__])
# ['W', 'Y', 'Z', 'X', 'object']
w = W()
w.hello()
# W.hello, 即将调用super
# Y.hello, 即将调用super
# Z.hello, 即将调用super ← 注意!Y的super()调用的是Z,不是X!
# X.hello
Y 里的 super().hello() 按 MRO 顺序找到的下一个是 Z,而不是 Y 自己声明时继承的 X------这正是 super() 存在的意义:它让每个类不需要知道自己在继承链条里的确切位置,只管"调用下一棒",由 Python 在运行时根据完整的 MRO 决定"下一棒"到底是谁。
如果不用 super(),而是像下面这样把父类名字写死,会直接漏掉 Z:
python
class Y2(X):
def hello(self):
print("Y2.hello")
X.hello(self) # 硬编码父类,不走MRO
class Z2(X):
def hello(self):
print("Z2.hello")
X.hello(self)
class W2(Y2, Z2):
def hello(self):
print("W2.hello")
super().hello()
W2().hello()
# W2.hello
# Y2.hello
# X.hello ← Z2.hello 根本没有被调用到!
这就是为什么多重继承场景下必须用 super(),而不是硬编码父类名------硬编码会打破 MRO 协作链条,导致钻石继承结构里的另一支被跳过。
2. 手动模拟 C3 合并算法
C3 算法的核心是一个 merge 操作:把若干个列表合并成一个,每一步从各列表头部找一个"好头部"(good head)------即这个类不出现在任何列表的非头部位置(尾部)------放进结果里,找不到就说明无法构造出一致的顺序,直接报错。
python
def merge(*lists):
lists = [list(l) for l in lists]
result = []
while True:
lists = [l for l in lists if l]
if not lists:
return result
for l in lists:
head = l[0]
if not any(head in tail[1:] for tail in lists): # 不在任何列表的尾部出现
result.append(head)
for l2 in lists:
if l2 and l2[0] == head:
l2.pop(0)
break
else:
raise TypeError("Cannot create a consistent MRO")
# D(B, C) 的 MRO = D + merge(L[B], L[C], [B, C])
L_B = ["B", "A", "object"]
L_C = ["C", "A", "object"]
mro_D = ["D"] + merge(L_B, L_C, ["B", "C"])
print(mro_D) # ['D', 'B', 'C', 'A', 'object']
print(mro_D == [c.__name__ for c in D.__mro__]) # True ------ 和 Python 实际计算的结果完全一致
3. MRO 计算失败的场景
如果继承关系本身就自相矛盾(比如同时要求 A 排在 B 前面,又要求 B 排在 A 前面),C3 算法找不到"好头部",会直接在类定义阶段报错:
python
class M(A, B): # A在前,但B又继承自A,两个约束互相冲突
pass
# TypeError: Cannot create a consistent method resolution order (MRO) for bases A, B
这里 class M(A, B) 声明顺序要求 A 排在 B 前面,但 B 继承自 A,MRO 规则又要求子类必须排在父类前面(B 必须排在 A 前面)------两个约束互相矛盾,C3 算法无解,Python 直接拒绝创建这个类,而不是含糊地给出一个可能不符合预期的顺序。
四、面试追问
Q1:什么是 MRO?
MRO(Method Resolution Order,方法解析顺序)是 Python 为每个类计算出的一条线性顺序,决定了在多重继承场景下,调用同名方法/访问同名属性时,应该按照怎样的顺序去各个基类里查找。可以用 ClassName.__mro__ 或 ClassName.mro() 直接查看这个顺序。
Q2:Python 怎么计算 MRO?
用 C3 线性化算法计算,保证三条性质:子类永远排在父类前面(单调性)、一个类的多个父类之间按声明顺序排列、每个类在整条 MRO 里只出现一次。这避免了简单深度优先遍历会导致的公共基类被重复访问、顺序违反直觉等问题。
Q3:super() 调用的是父类吗?
不完全是。super().method() 调用的是当前类在 MRO 链条里的下一个类 ,而不是当前类自己声明时继承的那个父类。在单继承场景下两者恰好是同一个类,容易造成误解;但在多重继承场景下,MRO 里的下一个类可能是"兄弟"分支上的另一个类,这正是 super() 能让多重继承里的所有类都被正确调用到的关键机制。
Q4:钻石继承问题,C3 线性化是怎么解决的?
C3 线性化保证钻石结构顶端的公共祖先类在 MRO 里只出现一次,且排在所有依赖它的子类之后,这样配合 super() 逐级"传递调用",可以保证钻石继承结构里的每一个分支都恰好被调用一次,既不会重复执行公共祖先的逻辑,也不会漏掉某个分支。
Q5:什么情况会导致 MRO 计算失败?
当继承声明本身包含互相矛盾的顺序要求时会失败,比如显式声明某个类的基类列表要求 A 排在 B 前面,但 B 本身又是 A 的父类(子类必须排父类前面的规则要求 B 在 A 前面),两条规则互相冲突,C3 算法找不到能同时满足所有约束的顺序,Python 会在类定义阶段直接抛出 TypeError,提示无法为这些基类构造出一致的 MRO。
下一篇预告
Day14 讲元类 metaclass------Python 面向对象里的"终极武器",类本身是怎么被创建出来的,以及元类能用来做哪些普通继承做不到的事情。