前言
MRO 是 Method Resolution Order 的缩写,中文常译作"方法解析顺序"。它回答一个问题:当一个实例调用某个方法时,Python 按什么顺序去各个类里找它?
单继承时这个问题看起来没什么可讲的——子类没有就往上找父类,一条直线。但 Python 支持多重继承,一个类可能有两条甚至更多路径通向同一个祖先,这时"往上找"就不再是一条直线了。文档把这种结构称为菱形关系(diamond relationship):因为所有类最终都继承object,任何多重继承都天然构成菱形。
Python 的解法是C3 线性化:把一个类及其所有祖先排成一个不重复的线性序列。围绕它常见的误解有两个:
- 以为多重继承的方法是"深度优先、从左到右"地全跑一遍。实际每个类只会被选中一次,
super()靠这个线性序列接力。 - 以为 MRO 只看继承列表的书写顺序。实际上它还受每个基类自身的继承结构影响,光看
class D(B, C)猜不准。
本文讲 C3 的规则、怎么查看 MRO,以及它和super()的关系。
一、MRO 要解决什么
假设有这样一组类:
A
/ \
B C
\ /
DB和C都继承A,D又同时继承B和C。如果按"深度优先、从左到右"去找方法:先找D,再找B,再找B的父类A——可这时C还没被找过,而A已经被找了两遍。谁先谁后、会不会重复,就说不清了。
文档对 Python 采用的算法的描述是:它保持每个类声明时的从左到右顺序、每个父类只被访问一次、并且是单调的(即一个类被继承后不会打乱已有父类的优先级)。这套规则让多重继承的行为变得可预测。
二、C3 线性化的结果长什么样
先看最经典的菱形:
# 适用于 Python 3.8+
class A:
pass
class B(A):
pass
class C(A):
pass
class D(B, C):
pass
print([cls.__name__ for cls in D.__mro__])
# ['D', 'B', 'C', 'A', 'object']顺序是D → B → C → A → object。注意两点:
B和C谁在前面,由class D(B, C)的书写顺序决定;A排在两者之后,而不是紧跟B——这正是线性化避免重复访问的手段。
下表用"查找某个方法时依次去哪些类找"的角度,列出几种继承结构的 MRO:
| 类定义 | MRO |
|---|
class B(A) | B, A, object |
class C(A) | C, A, object |
class D(B, C) | D, B, C, A, object |
class E(C, B) | E, C, B, A, object |
class F(D, E) | 冲突,抛TypeError |
最后一行值得说明:D要求B在C前,E要求C在B前,两者矛盾,C3 无法给出一个一致序列,于是 Python 直接报TypeError: Cannot create a consistent method resolution order (MRO)。
三、查看 MRO:__mro__ 与 mro()
每个类都有__mro__属性,它是一个元组,列出方法解析时会依次检查的类:
# 适用于 Python 3.8+
class A:
pass
class B(A):
pass
print(B.__mro__)
# (<class '__main__.B'>, <class '__main__.A'>, <class 'object'>)类上还有mro()方法,返回的是列表形式。文档说明mro()可以被元类重写,用于自定义 MRO,它在类创建时被调用,结果存进__mro__。
# 适用于 Python 3.8+
print(B.mro())
# [<class '__main__.B'>, <class '__main__.A'>, <class 'object'>]排查继承问题时,第一件事就是打印类名.__mro__——它比任何推测都可靠。另外__bases__给出的是直接基类的元组,__mro__才是完整链条,两者别混用。
四、MRO 与 super() 的配合
这是 MRO 最实际的用途。super()返回一个代理对象,从 MRO 中当前类之后的位置继续搜索。文档举的例子是:如果__mro__是D -> B -> C -> A -> object而type是B,那么super()会在C -> A -> object里找。
# 适用于 Python 3.8+
class Base:
def handle(self):
return ["Base"]
class AuthMixin:
def handle(self):
return ["Auth"] + super().handle()
class LogMixin:
def handle(self):
return ["Log"] + super().handle()
class API(AuthMixin, LogMixin, Base):
pass
print([c.__name__ for c in API.__mro__])
# ['API', 'AuthMixin', 'LogMixin', 'Base', 'object']
print(API().handle())
# ['Auth', 'Log', 'Base']AuthMixin.handle里的super()跳到了LogMixin——这是它的MRO 后继,不是它的"父类"(AuthMixin的父类其实是object)。这里就体现出 MRO 与super()是一体的:不知道 MRO,就预测不出super()会跳到哪。
如果某个环节忘了写super(),它后面的所有实现都会被静默跳过,程序不报错:
# 适用于 Python 3.8+
class LogMixin:
def handle(self):
return ["Log"] # 没调 super(),Base 永远轮不到五、MRO 冲突怎么排查
出现Cannot create a consistent method resolution order (MRO)时,说明继承结构本身矛盾,典型是"把祖先排在了后代后面":
# 适用于 Python 3.8+
class A:
pass
class B(A):
pass
# class C(A, B): # TypeError: Cannot create a consistent MRO
# passB已经是A的子类,class C(A, B)却要求A排在B前面,这与A必须在B之后相矛盾。
排查步骤建议固定成三条:
- 写出每个涉及类的
__bases__,画成图; - 从上到下检查有没有"祖先排在后代之后"的写法;
- 试着删掉某个基类,看是否是因为它把两条无关的链条强行合并导致矛盾。
常见坑点
1. 以为super()指的就是直接父类
❌ 在多重继承里断言AuthMixin.handle里的super()会调到自己的父类。
✅ 它调到的是当前实例的 MRO 中当前类之后的下一个类,要打印__mro__才能确定。
2. 用"深度优先"预测顺序
❌ 以为D(B, C)会先把B的整条祖先链找完再找C。
✅ C3 的输出是D, B, C, A, object——A被排到C之后。
3. 在嵌套函数 / 生成器表达式里用零参super()
❌ 以为super()在哪都能自动认出当前类和self,于是在方法内部又套一层函数时照写super()。
✅ 零参super()靠编译器在外围方法里填入当前类与第一个参数,嵌套函数里不成立(生成器表达式也会隐式生成嵌套函数);这种场合要写显式两参形式super(当前类, self)。
4. 以为继承列表相同,MRO 就相同
❌ 看到两个类都写成(X, Y),就断定它们的解析顺序完全一致。
✅ 顺序还取决于X、Y各自继承了谁;上游结构一变,下游顺序可能跟着变,必须打印__mro__才算数。
5. 用__bases__当完整链条
❌ 打印D.__bases__只看到(B, C),就以为这是全部顺序。
✅__bases__是直接基类;完整顺序看__mro__。
6. MRO 冲突时反复调换顺序试错
❌ 遇到TypeError就把继承列表换来换去,直到不报错。
✅ 冲突说明结构矛盾(祖先排在了后代之后),需要重新设计层次。
7. 在子类里给__mro__赋值
❌ 运行中直接SomeClass.__mro__ = ...想强行改顺序。
✅__mro__是只读的;要自定义解析顺序只能在元类里重写mro(),且在类创建前生效。
8. 忘了每个类的 MRO 末尾都有object
❌ 统计链长时漏算object,或以为重写object的方法能救场。
✅ 所有类的 MRO 都以object结尾,多重继承里它只出现一次,这也是"任意多重继承都是菱形"的原因。
总结
| 概念 | 说明 |
|---|
| MRO | 属性/方法查找的顺序 |
| 算法 | C3 线性化,保持从左到右、每个类一次、单调 |
| 查看方式 | 类.__mro__(元组)、类.mro()(列表) |
super() | 从 MRO 中当前类之后继续搜索 |
| 冲突表现 | TypeError: Cannot create a consistent MRO |
MRO 不是一条只供阅读的规则,它是super()行为的依据。想预测一段多重继承代码会调到哪个方法,唯一可靠的办法就是打印__mro__,再顺着序列看每个类的实现是不是都调了super()。把这两步养成习惯,多重继承就不再是黑箱。