钻石继承调用哪个方法?一文讲透 MRO 与 C3 线性化算法

「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 同时继承 BC,而 BC 又都继承自 A 的结构,就是经典的钻石继承(菱形继承)

flowchart TB A[A] --> B[B] A --> C[C] B --> D[D] C --> D

如果简单粗暴地用"深度优先"去遍历(先一路找到底: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
flowchart LR D2[W] --> B2[Y] --> C2[Z] --> A2[X] --> O2[object]

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 面向对象里的"终极武器",类本身是怎么被创建出来的,以及元类能用来做哪些普通继承做不到的事情。

相关推荐
码事漫谈1 小时前
国产替代的硬核样本:金仓数据库如何撑起固井软件的数据底座
后端
kevinnett1 小时前
图片生成跑到一半“失踪”了:我重新设计了异步任务状态机
python
小白勇闯网安圈1 小时前
Django 模板复用、ORM 查询与多对多关系
数据库·python·django
老孙讲技术1 小时前
【4G IPC 上云】临时点位怎么免布线上云?listDeviceDetailsByPage + 辅码流预览|智慧工地实战
后端·物联网
TheBestRucy1 小时前
基于Dify的旅游攻略&王者荣耀攻略智能助手项目
服务器·开发语言·人工智能·python·算法·旅游
天才少女爱迪生1 小时前
KIMI-K3技术博客写作思路分析
python
丨白色风车丨2 小时前
MCP 入门指南:大模型时代的“USB-C”接口
python·mcp
铁皮饭盒2 小时前
DeepSeek V4 Pro 0813发布了, 也可以部署到 Codex 了
前端·javascript·后端
EXI-小洲2 小时前
Web Spider 某渣渣企业平台 表单参数逆向 Webpack
python·webpack·js逆向·spider