重学python之mro
文章目录
- 1. 背景
- 2. MRO是什么?
- 3. 继承规则
- 3.1 MRO 决定初始化顺序
- 4. 如果父类有不同参数的__init__?
- 5. 如何让多继承的子类调用所有父类的__init__?
- 6. 附录
- a. C3 线性化算法
1. 背景
先看这么一个问题?
python的继承是可以继承父类的属性和函数的,而python支持多继承,假设类C继承于类A和类B,那么,类C是否同时包含了A和B的初始化函数__init__?, 类C是否既可以按照类A的构造方法初始化,也可以按照类B的构造函数初始化?
答案是:只会继承其中一个父类的 __init__ 方法
。具体继承哪个父类的 init,取决于 Python 的方法解析顺序(MRO,Method Resolution Order)。
2. MRO是什么?
MRO,Method Resolution Order,中文意思为方法解析顺序,在python中, __mro__
是 Python 中用于表示方法解析顺序(Method Resolution Order) 的特殊属性。它是一个元组,存储了类在多继承时查找方法(包括属性)的顺序。
当一个类继承自多个父类时,Python 需要确定:当调用一个方法或访问一个属性时,应该按照什么顺序去父类中查找。mro 正是这个查找顺序的具体体现。
适用场景: 主要用于多继承,解决 “菱形继承”(多个父类最终继承自同一个基类)等复杂继承关系中的方法查找歧义。
查找规则:
- python 会按照 mro 中类的顺序依次查找方法 / 属性,找到第一个匹配项后立即返回。
- 对于单继承,mro 顺序就是 “子类 → 父类 → 祖父类 → … → object”。
- 对于多继承,mro 遵循 “C3 线性化算法”,确保查找顺序的一致性和合理性。
示例
class A:passclass B(A):passclass C(A):passclass D(B, C): # 多继承 B 和 Cpass
</pre>
# 查看 D 类的方法解析顺序
print(D.__mro__)
# 输出:(D, B, C, A, object)
可以通过 类名.mro 或 inspect.getmro(类名) 查看任意类的方法解析顺序。
3. 继承规则
- 单继承遵循 “深度优先”
子类 → 直接父类 → 祖父类 → … → 顶层基类(如 object),形成线性查找链。 - 多继承遵循 C3 线性化算法
核心是生成一个无歧义的方法查找顺序,确保:- 保持子类声明的父类顺序(如 class C(A, B) 中 A 优先于 B)
- 父类的 MRO 顺序在子类中保持单调性(不颠倒祖先类顺序)
- 每个类在 MRO 中仅出现一次
- __mro__属性存储实际顺序
通过 类名__mro__可查看具体顺序,查找方法 / 属性时严格按此顺序执行,找到第一个匹配项即停止。 - super() 函数依赖 MRO
super() 并非简单指向 “父类”,而是根据当前类的 MRO 顺序,返回下一个类的实例,用于调用继承链中的方法。 - 非法继承关系会直接报错
若 C3 算法无法生成合法的 MRO(如出现循环依赖或顺序冲突),Python 会抛出 TypeError 提示无法创建一致的方法解析顺序。
3.1 MRO 决定初始化顺序
- 多继承时,Python 会按照 MRO 规定的顺序查找父类方法。对于 init 来说,只会执行 MRO 中第一个出现的、有 init 方法的父类的构造函数。
示例
class A:def __init__(self):print("A 的 __init__ 被调用")class B:def __init__(self):print("B 的 __init__ 被调用")# 多继承:C 继承 A 和 B,自身无 __init__
class C(A, B):pass # 无自定义 __init__# 创建 C 的实例
c = C() # 输出:"A 的 __init__ 被调用"
原因:
- C 的 MRO 顺序是 [C, A, B, object](可通过 C.mro 查看)。
- 解释器会先在 A 中找到 init 并执行,不会再去调用 B 的 init。
4. 如果父类有不同参数的__init__?
若父类 A 和 B 的 init 有不同参数,直接实例化 C 可能报错:
class A:def __init__(self, x):print(f"A 的 __init__,x={x}")class B:def __init__(self, y):print(f"B 的 __init__,y={y}")class C(A, B):pass# 报错:A 的 __init__ 需要参数 x,但未提供
c = C() # TypeError: A.__init__() missing 1 required positional argument: 'x'# 只能按 MRO 中第一个 __init__(A)的参数传参
c = C(x=1) # 输出:"A 的 __init__,x=1"(B 的 __init__ 仍不会被调用)
5. 如何让多继承的子类调用所有父类的__init__?
需在子类 C 中显式定义 init,并通过 super() 或直接调用父类 init 来触发:
class C(A, B):def __init__(self, x, y):# 调用 A 的 __init__A.__init__(self, x)# 调用 B 的 __init__B.__init__(self, y)c = C(x=1, y=2)
# 输出:
# A 的 __init__,x=1
# B 的 __init__,y=2
6. 附录
a. C3 线性化算法
C3 算法的核心是为每个类生成一个线性化列表(linearization),这个列表是类本身、父类的线性化列表及父类声明顺序的 “合并结果”,需
满足以下原则:
- 保持父类顺序:子类声明的父类顺序(如 class D(B, C) 中 B 在 C 前)必须在结果中保持。
- 单调性:若类 X 是类 Y 的祖先,则 X 在 Y 的线性化列表中出现的位置,必须在所有 Y 的子类的线性化列表中 X 出现的位置之前。
- 唯一性:每个类在线性化列表中仅出现一次。
C3 算法的计算步骤
为类 C 计算线性化列表(记为 L©)的步骤如下:
- 初始化:L© 以 C 本身为起点,即 L© = [C] + merge(L(B1), L(B2), …, L(Bn), [B1, B2, …, Bn]),其中 B1, B2, …, Bn 是 C 直接继承的父类(按声明顺序)。
- 合并操作(merge):合并多个列表(父类的线性化列表 + 父类声明顺序列表),规则是:
- 从第一个列表的头部取一个类 X;
- 检查 X 是否在其他所有列表的尾部(即 X 不在其他列表的非尾部位置);
- 若满足,将 X 加入结果,并从所有列表中移除 X;
- 若不满足,取下一个列表的头部重试;
- 重复直到所有列表为空(若无法合并则报错,说明继承关系不合理)。
示例
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass # D 继承 B 和 C,B 和 C 都继承 A
计算各 class 的线性化列表:
- L(A):A 没有父类(除 object),所以 L(A) = [A, object]。
- L(B):B 继承 A,因此:
L(B) = [B] + merge(L(A), [A])L(A) = [A, object],[A] 是父类声明列表merge([A, object], [A]) → 取第一个列表的头部 A,检查是否在其他列表([A])的尾部(是),加入结果并移除 A → 剩余 [object] 和 []继续合并 [object] 和 [] → 加入 object → 最终 L(B) = [B, A, object]
- L(C):类似 B,L(C) = [C, A, object]。
- L(D):D 继承 B 和 C,因此:
L(D) = [D] + merge(L(B), L(C), [B, C])L(B) = [B, A, object],L(C) = [C, A, object],[B, C] 是父类声明列表第一步:检查第一个列表(L(B))的头部 B,是否在其他列表(L(C) = [C, A, object]、[B, C])的尾部?L(C) 中 B 未出现(满足);[B, C] 中 B 是头部(非尾部,不满足)→ 不能选 B。第二步:取下一个列表(L(C))的头部 C,检查是否在其他列表(L(B) = [B, A, object]、[B, C])的尾部?L(B) 中 C 未出现(满足);[B, C] 中 C 是尾部(满足)→ 选 C,加入结果并移除所有列表中的 C。剩余列表:L(B) = [B, A, object],L(C) = [A, object],[B]第三步:检查第一个列表头部 B,是否在其他列表(L(C) = [A, object]、[B])的尾部?L(C) 中 B 未出现(满足);[B] 中 B 是尾部(满足)→ 选 B,加入结果并移除 B。剩余列表:L(B) = [A, object],L(C) = [A, object],[]第四步:检查第一个列表头部 A,是否在其他列表(L(C) = [A, object])的尾部?是 → 选 A,移除后剩余 [object] 和 [object]。第五步:加入 object → 最终 L(D) = [D, C, B, A, object]
因此,D.mro 的结果为 (D, C, B, A, object),这就是 C3 算法计算的方法查找顺序。