Python 基础体系 · 第 24/112 篇。示例统一以 Python 3.14 为语言基线;第三方库使用与其兼容的现代稳定版本,版本敏感行为会单独说明。

Python MRO 与 super:C3 线性化、多继承和协作调用

在 Python 中,多继承不是“依次调用多个父类”的语法糖。它包含两个彼此关联、但职责不同的机制:

  • MRO(Method Resolution Order,方法解析顺序):规定从一个类及其基类中按什么顺序查找属性和方法。
  • super():按照当前对象的 MRO,从指定类之后继续查找属性,并将查找结果绑定到当前对象或类。

因此,理解 super() 不能只记住“调用父类方法”。更准确的模型是:

super() 不指向某个固定的父类,而是在一条已经计算好的 MRO 上,从某个位置继续查找。

Python 3 使用 **C3 线性化(C3 linearization)**计算 MRO。C3 试图同时满足局部优先顺序、单调性和祖先唯一性;如果继承关系中的约束互相矛盾,类会在创建阶段直接失败。Python 官方数据模型文档规定,类属性查找使用 C3 MRO;类的 __mro__ 保存实际参与方法解析的类序列。(docs.python.org)


一、先区分三个容易混淆的概念

1. 多继承是类声明关系

class Child(Left, Right):
    pass

这里的 Child 有两个直接基类:

Child.__bases__ == (Left, Right)

__bases__ 只表示直接继承关系,不表示完整的方法查找顺序。

2. MRO 是完整的查找顺序

Child.__mro__

它返回一个元组,包含 Child、其基类以及最终的 object,顺序从最优先到最不优先。

例如:

class A:
    pass

class B(A):
    pass

class C(A):
    pass

class D(B, C):
    pass

print(D.__bases__)
print(D.__mro__)

输出:

(<class '__main__.B'>, <class '__main__.C'>)
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

D.__bases__ 是:

B, C

D.__mro__ 是:

D, B, C, A, object

后者才是 D 实例进行普通属性查找时使用的顺序。

3. super() 是沿 MRO 移动的代理

下面这句代码:

super().run()

不是简单地翻译为:

Parent.run(self)

它的含义更接近:

  1. 找到当前方法所在的类;
  2. 找到当前对象的实际类型;
  3. 取该类型的 MRO;
  4. 从当前类在 MRO 中出现的位置之后继续查找 run
  5. 找到后,将它绑定到当前对象并调用。

官方文档将 super 描述为一个代理对象:它把方法调用委托给指定类之后的父类或“兄弟类”;查找从 object_or_type 对应类的 MRO 中、指定类之后的位置开始。(docs.python.org)


二、为什么多继承需要 MRO

1. 单继承时,顺序天然明确

class A:
    def run(self):
        return "A"

class B(A):
    pass

class C(B):
    pass

查找 C().run 时,顺序是:

C -> B -> A -> object

如果 CB 都没有 run,最终使用 A.run

单继承的 MRO 可以直接写成:

L(C) = [C, B, A, object]

2. 多继承会产生多个路径

考虑经典菱形结构:

       A
      / \
     B   C
      \ /
       D

代码是:

class A:
    def run(self):
        return "A"

class B(A):
    def run(self):
        return "B"

class C(A):
    def run(self):
        return "C"

class D(B, C):
    pass

DA 存在两条路径:

D -> B -> A
D -> C -> A

如果简单使用“深度优先、从左到右”的查找方式,就可能重复经过 A,或者在某些更复杂的结构中违反用户明确写出的基类顺序。

Python 需要一种线性化算法,把有向无环的继承图转换为一条线性序列,同时保留重要约束:

  1. 局部优先顺序class D(B, C) 中,B 必须排在 C 前面。
  2. 父类内部顺序:如果 B 的 MRO 中 XY 前面,那么合并到子类时不能反过来。
  3. 单调性:子类不能悄悄改变已有父类之间的优先顺序。
  4. 祖先不重复:同一个类在最终 MRO 中只出现一次。

C3 线性化就是用来满足这些约束的。


三、C3 线性化的形式化定义

设类 C 直接继承自:

B1, B2, ..., BN

L(C) 为类 C 的线性化结果,也就是它的 MRO。

C3 的递归公式是:

L(C) = [C] + merge(L(B1), L(B2), ..., L(BN), [B1, B2, ..., BN])

其中:

  • [C] 表示只包含 C 的列表;
  • L(Bi) 是每个直接基类的 MRO;
  • 最后一个列表 [B1, ..., BN] 是当前类声明时的直接基类顺序;
  • merge 负责在不违反已有顺序的情况下合并这些列表。

object 是递归终点:

L(object) = [object]

这正是 Python 官方 C3 MRO 文档给出的基本规则。(docs.python.org)

1. head 和 tail

对一个列表:

[A, B, C]
  • head 是第一个元素:A
  • tail 是其余元素:[B, C]

C3 每次从若干列表的头部选择一个候选类。

2. 什么是“合法候选”

候选类必须满足:

它出现在某个列表的头部,但不能出现在任何其他列表的尾部。

换句话说,如果候选类已经被另一个列表明确排在后面,就不能现在选它。

伪代码如下:

merge(sequences):
    result = []

    while 仍有非空列表:
        candidate = None

        for sequence in sequences:
            if sequence 为空:
                continue

            head = sequence[0]

            if head 不在任何其他序列的 tail 中:
                candidate = head
                break

        if candidate is None:
            继承约束冲突,无法创建类

        result.append(candidate)

        从所有序列的头部删除 candidate

这里的“从所有序列的头部删除”很重要:同一个类可能同时出现在多个列表头部,但它只能被加入最终结果一次。


四、完整推导:菱形继承如何得到 MRO

使用下面的层次结构:

class A:
    pass

class B(A):
    pass

class C(A):
    pass

class D(B, C):
    pass

先计算较小类的线性化。

1. A

L(A) = [A, object]

2. B

L(B) = [B] + merge(L(A), [A])
       = [B] + merge([A, object], [A])
       = [B, A, object]

因此:

L(B) = [B, A, object]

同理:

L(C) = [C, A, object]

3. D

D 继承自 B, C,因此:

L(D) = [D] + merge(
    [B, A, object],
    [C, A, object],
    [B, C]
)

现在逐步合并。

第一步

当前列表:

[B, A, object]
[C, A, object]
[B, C]

候选依次是:

B
C
B

检查第一个候选 B

  • B 是第一个列表的 head;
  • B 不在其他列表的 tail 中。

因此选择 B

结果: [B]

删除各列表头部的 B

[A, object]
[C, A, object]
[C]

第二步

候选:

A
C
C

先看 A

  • A 出现在第一个列表的 head;
  • A 出现在第二个列表的 tail [A, object] 中。

因此 A 不是合法候选。

再看 C

  • C 是第二个列表和第三个列表的 head;
  • C 不在其他列表的 tail 中。

选择 C

结果: [B, C]

删除头部的 C

[A, object]
[A, object]
[]

第三步

现在两个非空列表都是:

[A, object]
[A, object]

选择 A

结果: [B, C, A]

继续删除:

[object]
[object]
[]

最后选择 object

结果: [B, C, A, object]

加上 D 本身:

L(D) = [D, B, C, A, object]

运行验证:

print([cls.__name__ for cls in D.__mro__])

输出:

['D', 'B', 'C', 'A', 'object']

这里的关键不是“先遍历 B 的整条链,再遍历 C 的整条链”,而是:

C3 在多个已排序序列之间做受约束的合并。

因此,A 虽然在 B 的继承链中较早出现,但由于它仍然位于 C 的 MRO 尾部,不能抢先于 C 被选中。


五、C3 的两个关键性质

1. 保留局部优先顺序

class D(B, C):
    pass

要求:

B < C

这里的 < 表示在 MRO 中“更靠前”。

最终结果:

D, B, C, A, object

确实保留了 BC 前面的声明顺序。

如果交换基类:

class D(C, B):
    pass

通常会得到:

D, C, B, A, object

因此,基类列表不是装饰性写法。改变:

class D(B, C)

为:

class D(C, B)

会改变方法解析和 super() 调用顺序。

2. 保持单调性

所谓单调性,可以直观理解为:

如果一个类已经决定 X 优先于 Y,那么继续创建它的子类时,不应无故把 Y 放到 X 前面。

例如:

class A:
    pass

class B(A):
    pass

class C(A):
    pass

class Parent(B, C):
    pass

class Child(Parent):
    pass

有:

Parent.__mro__ == (Parent, B, C, A, object)
Child.__mro__  == (Child, Parent, B, C, A, object)

Child 只是在前面增加了自身和 Parent,没有破坏 BCA 原有的相对顺序。


六、继承关系不一致时,类创建会失败

C3 不会为矛盾的继承约束“猜一个顺序”。

class X:
    pass

class Y:
    pass

class A(X, Y):
    pass

class B(Y, X):
    pass

此时:

L(A) = [A, X, Y, object]
L(B) = [B, Y, X, object]

如果继续声明:

class C(A, B):
    pass

理论上需要合并:

[A, X, Y, object]
[B, Y, X, object]
[A, B]

先选 A,再选 B,剩下:

[X, Y, object]
[Y, X, object]

此时:

  • 选择 X 会违反第二个列表中 YX 前面的顺序;
  • 选择 Y 会违反第一个列表中 XY 前面的顺序。

没有合法候选,因此类定义失败:

TypeError: Cannot create a consistent method resolution
order (MRO) for bases X, Y

示例:

class X:
    pass

class Y:
    pass

class A(X, Y):
    pass

class B(Y, X):
    pass

try:
    class C(A, B):
        pass
except TypeError as exc:
    print(type(exc).__name__)
    print(exc)

预期输出的核心内容是:

TypeError
Cannot create a consistent method resolution order (MRO) for bases X, Y

失败发生在 C 创建阶段,而不是调用 C() 或访问方法时。这是一个重要边界:

MRO 是类对象创建时计算的;继承约束不一致,类本身就不会成功创建。


七、普通属性查找如何使用 MRO

对实例表达式:

obj.name

Python 会根据对象的类型及其继承关系查找属性。对于类属性和方法,基类搜索使用 C3 MRO;若找到用户定义函数,还会继续发生描述符绑定,把函数转换为绑定方法。(docs.python.org)

示例:

class A:
    value = "A"

class B(A):
    pass

class C(A):
    value = "C"

class D(B, C):
    pass

d = D()

print(d.value)
print(D.__mro__)

输出:

C
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

查找过程是:

  1. D.__dict__ 中没有 value
  2. B.__dict__ 中没有 value
  3. C.__dict__ 中找到 value
  4. 返回 "C"
  5. 不再继续查找 A.value

MRO 决定的是第一个匹配项。它不会把所有父类中同名属性合并起来。


八、super() 的准确语义

1. 两参数形式

super 的形式可以写成:

super(type, object_or_type)

例如:

super(B, self)

它表示:

self 的实际类型的 MRO 中,找到 B,然后从 B 后面继续搜索。

假设:

type(self).__mro__ =
D -> B -> C -> A -> object

则:

super(B, self).run()

搜索:

C -> A -> object

而不是搜索:

B -> A

官方文档明确说明,第二个参数决定使用哪一个对象或类型的 MRO,搜索从第一个参数指定的类之后开始。(docs.python.org)

2. super() 不是“父类对象”

下面的理解是不准确的:

super() == 父类

super() 返回的是一个代理对象。它自身不复制父类,也不把某个固定父类绑定为“唯一目标”。

这也是为什么同一段代码在不同最终子类中可能调用不同的下一个类。

3. 零参数形式

在普通实例方法中:

class Child(Parent):
    def run(self):
        return super().run()

等价于:

class Child(Parent):
    def run(self):
        return super(Child, self).run()

但这不是简单的文本替换。编译器会为使用 super()__class__ 的方法建立隐式 __class__ 闭包单元,以便零参数 super() 知道词法上定义该方法的类;实例方法中的第一个参数通常是当前实例。(docs.python.org)

可以通过闭包信息观察这一点:

class Parent:
    def run(self):
        return "parent"

class Child(Parent):
    def run(self):
        return super().run()

print(Child.run.__code__.co_freevars)

通常输出:

('__class__',)

4. 零参数 super() 的使用边界

它依赖当前方法的编译上下文,因此不能随意移动到嵌套函数中:

class Parent:
    def run(self):
        return "parent"

class Child(Parent):
    def run(self):
        def nested():
            return super().run()

        return nested()

调用:

Child().run()

会失败,因为 nested 并不是以当前实例方法的方式接收 self,也没有零参数 super() 所期望的调用上下文。

如果确实需要在嵌套函数中访问父类逻辑,应显式保存必要对象,或者使用显式参数形式,但仍需确保参数关系合法:

class Child(Parent):
    def run(self):
        proxy = super()

        def nested():
            return proxy.run()

        return nested()

这里 proxy 已经在外层普通方法中正确创建。


九、为什么 super() 能调用“兄弟类”

看一个协作调用的菱形结构:

class A:
    def process(self):
        print("A.process")


class B(A):
    def process(self):
        print("B.process: before")
        super().process()
        print("B.process: after")


class C(A):
    def process(self):
        print("C.process: before")
        super().process()
        print("C.process: after")


class D(B, C):
    pass


D().process()

先查看 MRO:

print([cls.__name__ for cls in D.__mro__])

输出:

['D', 'B', 'C', 'A', 'object']

调用过程如下:

  1. D().process()D 中找不到;
  2. B 中找到 B.process
  3. 进入 B.process
  4. super() 表示从 B 后面继续查找;
  5. D 的 MRO 中,B 后面是 C
  6. 调用 C.process
  7. C 中的 super()C 后面继续查找;
  8. 找到 A.process

输出:

B.process: before
C.process: before
A.process
C.process: after
B.process: after

这里 B.processsuper() 调用了 C.processCB 在最终 MRO 中的后继类,但不是 B 的直接父类。

这正是协作式多继承的核心:

每个类只负责调用 MRO 中的下一个实现,而不是直接点名某个父类。


十、显式父类调用为什么会破坏协作链

B 改成显式调用:

class B(A):
    def process(self):
        print("B.process: before")
        A.process(self)
        print("B.process: after")

再次执行:

D().process()

调用链变成:

D -> B -> A

C.process 被跳过,输出:

B.process: before
A.process
B.process: after

问题在于,B 不再遵循最终类 D 的 MRO,而是硬编码了 A

A.process(self)

显式父类调用有明确用途,例如:

  • 明确只想调用某个特定实现;
  • 正在处理不兼容的第三方基类;
  • 协作链不适用,且这种耦合是有意设计的。

但它不等同于 super()。在多继承协作结构中,二者的数据流不同:

super():
当前类 -> 当前对象 MRO 中的下一个实现

Parent.method(self):
当前类 -> 被硬编码的 Parent 实现

十一、协作式方法的必要条件

super() 只负责找到下一个方法,不负责自动解决参数协议。

因此,协作式方法通常必须满足以下条件。

1. 每个参与者都调用 super()

class A:
    def process(self):
        print("A")


class B(A):
    def process(self):
        print("B")
        super().process()


class C(A):
    def process(self):
        print("C")
        super().process()

如果某一环不调用 super(),调用链就在该处终止。

class BrokenC(A):
    def process(self):
        print("BrokenC")
        # 没有 super()

如果:

class D(B, BrokenC):
    pass

MRO 可能仍然合法,但运行时只会走到 BrokenC,之后的 A.process 不会执行。

因此:

MRO 保证顺序,不保证每个方法都愿意继续调用。

2. 方法签名必须兼容

不兼容的签名会在链条中产生运行时错误:

class A:
    def process(self, *, trace_id):
        print("A", trace_id)


class B(A):
    def process(self):
        print("B")
        super().process()

调用:

B().process()

会在 B 调用 A.process 时缺少 trace_id

协作式方法应共享一个明确的参数协议。例如使用关键字参数:

class A:
    def process(self, *, trace_id, **kwargs):
        print("A", trace_id)
        return super().process(trace_id=trace_id, **kwargs)


class B:
    def process(self, *, trace_id, **kwargs):
        print("B", trace_id)
        return super().process(trace_id=trace_id, **kwargs)


class C(B, A):
    pass

但这里还有一个问题:A 继续调用 super().process(...) 时,MRO 最终会到达 object,而 object 没有这个方法。因此应增加终止类,或者让最底层实现停止转发。

更常见的写法是让链条中的根方法显式结束:

class Root:
    def process(self, *, trace_id, **kwargs):
        print("Root", trace_id)


class A(Root):
    def process(self, *, trace_id, **kwargs):
        print("A", trace_id)
        super().process(trace_id=trace_id, **kwargs)


class B(Root):
    def process(self, *, trace_id, **kwargs):
        print("B", trace_id)
        super().process(trace_id=trace_id, **kwargs)


class C(B, A):
    pass

验证:

C().process(trace_id="req-42")

C 的 MRO 是:

C -> B -> A -> Root -> object

输出:

B req-42
A req-42
Root req-42

这里 Root 是协作链的终点。它消费参数,但不再调用 super()

3. 每个类只能消费自己理解的参数

如果多个 mixin 共享 **kwargs,每个类应删除或消费自己负责的参数,否则未知参数最终会传到不接受它的实现。

class LoggingMixin:
    def process(self, *, log_context=None, **kwargs):
        print("log:", log_context)
        return super().process(**kwargs)


class RetryMixin:
    def process(self, *, retries=0, **kwargs):
        print("retries:", retries)
        return super().process(**kwargs)


class Root:
    def process(self, **kwargs):
        if kwargs:
            raise TypeError(f"unexpected arguments: {kwargs}")
        print("done")


class Service(RetryMixin, LoggingMixin, Root):
    pass


Service().process(log_context="request", retries=2)

执行顺序由 MRO 决定:

Service -> RetryMixin -> LoggingMixin -> Root

输出:

retries: 2
log: request
done

RetryMixin 消费了 retriesLoggingMixin 消费了 log_context,最后 Root 检查是否还有未处理参数。


十二、__init__ 中的协作式多继承

构造函数是多继承中最容易出问题的地方之一。

1. 一个不协作的基类会截断初始化

class Root:
    def __init__(self, **kwargs):
        print("Root.__init__")
        super().__init__(**kwargs)


class A(Root):
    def __init__(self, *, a, **kwargs):
        print("A.__init__", a)
        super().__init__(**kwargs)


class B(Root):
    def __init__(self, *, b, **kwargs):
        print("B.__init__", b)
        super().__init__(**kwargs)


class C(B, A):
    pass

调用:

C(a=1, b=2)

MRO:

C -> B -> A -> Root -> object

调用过程:

B.__init__(b=2)
A.__init__(a=1)
Root.__init__()
object.__init__()

输出:

B.__init__ 2
A.__init__ 1
Root.__init__

C 没有定义 __init__,所以查找从 B.__init__ 开始。BA 都必须:

  • 接收自己需要的参数;
  • 把其余参数继续传给 super()
  • 不要直接调用固定父类。

2. object.__init__ 不接受任意关键字参数

下面的代码有风险:

class Root:
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

如果 kwargs 中还有参数,最终传给 object.__init__ 时会报错。因此根类通常应当:

class Root:
    def __init__(self, **kwargs):
        if kwargs:
            raise TypeError(f"unexpected arguments: {kwargs}")
        super().__init__()

这使参数错误尽早暴露,而不是悄悄丢失。

3. 不要假设“每个类的 __init__ 只会运行一次”

在正确的 C3 MRO 和协作调用前提下,同一个类通常只会在链条中出现一次,因此其方法也只会被调用一次。但是以下情况会破坏这个假设:

  • 某个类显式调用了多个父类;
  • 某个类绕过 super()
  • 一个类同时出现在硬编码调用和协作链中;
  • 第三方基类没有遵守相同的协作协议。

例如:

class A:
    def __init__(self):
        print("A")


class B(A):
    def __init__(self):
        print("B")
        super().__init__()


class C(B, A):
    def __init__(self):
        print("C")
        B.__init__(self)
        A.__init__(self)

输出:

C
B
A
A

A.__init__ 被执行了两次。MRO 本身没有重复 A;重复来自显式调用绕过了 MRO。


十三、super() 也适用于属性和描述符

super() 不只查找方法,也可以查找属性。

class Parent:
    value = 10


class Child(Parent):
    value = 20

    def parent_value(self):
        return super().value


print(Child().value)
print(Child().parent_value())

输出:

20
10

Child().value 是普通属性查找,从 Child 开始;super().value 则从 Child 之后开始,因此得到 Parent.value

对于描述符,super(A, a).x 会在 a.__class__.__mro__ 中从 A 后面寻找定义 x 的类,并按描述符规则绑定。官方数据模型文档把这称为 super binding。(docs.python.org)

例如:

class Descriptor:
    def __get__(self, instance, owner):
        return f"owner={owner.__name__}"


class Parent:
    item = Descriptor()


class Child(Parent):
    def read(self):
        return super().item


print(Child().read())

super().item 不是从实例字典中查找,而是通过 super 的属性访问机制定位到 Parent.__dict__["item"],然后执行描述符绑定。

super()[name] 不是普通 super 查找

super() 主要支持显式点号访问:

super().method()
super().value

不能把它理解为一个可以任意下标访问的映射。官方文档指出,super 的查找机制针对显式点号属性访问;像 super()[name] 这样的隐式或下标形式不属于同一套定义。(docs.python.org)


十四、classmethod 中的 super()

classmethod 的第一个参数是类,而不是实例:

class Parent:
    @classmethod
    def make(cls):
        return cls.__name__


class Child(Parent):
    @classmethod
    def make(cls):
        return f"Child -> {super().make()}"


print(Child.make())

输出:

Child -> Child

这里零参数 super() 的第二个绑定对象是 cls,因此适用于类方法。

也可以显式写成:

super(Child, cls).make()

第二参数是类型时,要求该类型是第一个参数的子类关系范围内的合法对象;这也是 classmethod 使用两参数 super 的典型场景。(docs.python.org)

staticmethod 没有隐含的 selfcls

class Parent:
    @staticmethod
    def make():
        return "parent"


class Child(Parent):
    @staticmethod
    def make():
        return super().make()

这种写法不能按实例方法或类方法的方式理解,因为静态方法没有隐含的第一个参数。若需要调用特定父类的静态方法,可以明确写出:

class Child(Parent):
    @staticmethod
    def make():
        return super(Child, Child).make()

不过这种形式会把 Child 硬编码为第二参数,通常不如直接调用明确的类属性来得直观。静态方法不适合依赖动态实例 MRO 的协作链,除非显式设计并测试过其绑定方式。


十五、super() 与方法定义位置有关,而不只与运行时类有关

考虑下面的代码:

class A:
    def run(self):
        return "A"


class B(A):
    def run(self):
        return "B -> " + super().run()


class C(B):
    pass


print(C().run())

调用 C().run() 时,实际执行的是 B.runB.run 中的零参数 super()B 作为当前定义类,再结合对象 C() 的实际类型查找。

因此,搜索是:

C.__mro__ = C -> B -> A -> object

B 之后开始:

A -> object

结果:

B -> A

如果进一步加入多继承:

class Mixin(A):
    def run(self):
        return "Mixin -> " + super().run()


class D(Mixin, B):
    pass

print([cls.__name__ for cls in D.__mro__])
print(D().run())

D 的 MRO 为:

D -> Mixin -> B -> A -> object

调用链是:

Mixin.run -> B.run -> A.run

所以输出:

['D', 'Mixin', 'B', 'A', 'object']
Mixin -> B -> A

Mixin.run 中的 super() 并不会固定跳到 A;它从 Mixin 后面继续沿 D 的 MRO 查找,于是先遇到 B

这也是 mixin 可以组合的根本原因。


十六、MRO 可以通过工具直接诊断

1. 查看完整 MRO

print(MyClass.__mro__)

或者:

print(MyClass.mro())

type.__mro__ 是实际方法解析使用的类元组;type.mro() 可以被元类覆盖,用于定制其实例类的 MRO,计算结果会存入 __mro__。(docs.python.org)

2. 查看某个方法来自哪里

def find_definition(cls, name):
    for current in cls.__mro__:
        if name in current.__dict__:
            return current
    return None


class A:
    def run(self):
        pass


class B(A):
    pass


class C(B):
    def run(self):
        pass


print(find_definition(C, "run"))
print(find_definition(C, "missing"))

输出类似:

<class '__main__.C'>
None

这个方法只检查类字典,不处理实例字典和动态 __getattr__,因此适合诊断“某个类层次中到底是谁定义了这个名字”,不等价于完整的 Python 属性访问过程。

3. 检查 super 的起点

class A:
    def run(self):
        return "A"


class B(A):
    def run(self):
        proxy = super()
        print(proxy)
        return proxy.run()


class C(B):
    pass


print(C().run())

proxy 记录了:

  • 当前定义类:B
  • 绑定对象:C() 实例

它会从 C 的 MRO 中 B 后面的位置继续查找。


十七、元类可以影响 MRO,但不能随意破坏协作假设

类创建过程中,元类的 mro() 方法可以定制方法解析顺序:

class Meta(type):
    def mro(cls):
        order = super().mro()
        print(f"computing MRO for {cls.__name__}")
        return order


class Base(metaclass=Meta):
    pass

默认元类 type 会计算 MRO;元类可以覆盖 mro(),其结果会保存到新类的 __mro__ 中。(docs.python.org)

但自定义 MRO 有严格边界:

  1. 返回值必须是合法的类序列;
  2. 必须满足 Python 类创建对继承关系的要求;
  3. 返回的顺序会影响普通属性查找和 super()
  4. 任何依赖固定 MRO 的 mixin 组合都可能受到影响。

因此,元类定制 MRO 不是普通的“排序钩子”。它会改变类的语义基础。一个类如果依赖协作式 super(),就必须把自定义 MRO 纳入测试范围。

此外,类定义中的基类表达式还可能通过 __mro_entries__() 在创建类前替换原始基类;这属于类创建阶段的另一层机制。替换完成后,C3 计算使用的是替换后的实际基类集合,而不是语法中最初写出的对象。(docs.python.org)


十八、常见误解与对应失败表现

误解一:super() 总是调用直接父类

错误示例:

class A:
    def run(self):
        print("A")


class B(A):
    def run(self):
        print("B")
        super().run()


class C(A):
    def run(self):
        print("C")
        super().run()


class D(B, C):
    pass

B 的直接父类是 A,但:

D.__mro__ == (D, B, C, A, object)

所以 B.run 中的 super() 调用的是 C.run,不是直接跳到 A.run

误解二:MRO 只影响方法,不影响属性

class A:
    value = "A"


class B(A):
    pass


class C(A):
    value = "C"


class D(B, C):
    pass


print(D().value)

结果是:

C

同一套 MRO 同时参与方法和类属性的查找。

误解三:调用了 super() 就会自动调用所有父类

如果中间类没有调用 super()

class A:
    def run(self):
        print("A")


class B(A):
    def run(self):
        print("B")
        # 链条在这里终止


class C(B):
    pass

那么:

C().run()

只输出:

B

super() 是显式的转发动作,不是由解释器自动插入到每个重写方法中。

误解四:改变基类顺序不会影响行为

class D1(B, C):
    pass

class D2(C, B):
    pass

这两个类通常有不同 MRO:

D1 -> B -> C -> A -> object
D2 -> C -> B -> A -> object

如果 BC 都定义了同名方法,或者都参与 super() 协作,行为会随之改变。

误解五:显式调用多个父类等价于协作式调用

Parent1.method(self)
Parent2.method(self)

这不是 MRO 链,而是两个硬编码调用。它可能造成:

  • 共同祖先重复调用;
  • 某些父类被跳过;
  • 初始化顺序与 MRO 不一致;
  • 未来增加 mixin 后需要手动修改所有调用点。

十九、什么时候适合使用协作式多继承

协作式多继承适合以下结构:

  • 多个类对同一个生命周期钩子增加独立行为;
  • 每个类都遵循相同的方法签名;
  • 每个类都调用 super()
  • 有明确的根实现;
  • MRO 和参数协议可以通过测试固定下来。

例如:

class HandlerRoot:
    def handle(self, request, **kwargs):
        return {"request": request, **kwargs}


class AuthMixin:
    def handle(self, request, **kwargs):
        user = request.get("user")
        if user is None:
            raise PermissionError("authentication required")
        return super().handle(request, user=user, **kwargs)


class TraceMixin:
    def handle(self, request, **kwargs):
        trace_id = request.get("trace_id", "unknown")
        print("trace_id:", trace_id)
        return super().handle(request, trace_id=trace_id, **kwargs)


class Endpoint(TraceMixin, AuthMixin, HandlerRoot):
    pass


print(Endpoint().handle({
    "user": "alice",
    "trace_id": "req-1",
}))

MRO:

Endpoint -> TraceMixin -> AuthMixin -> HandlerRoot -> object

调用数据流:

request
  -> TraceMixin 读取 trace_id
  -> AuthMixin 校验 user
  -> HandlerRoot 产生最终结果

输出类似:

trace_id: req-1
{'request': {'user': 'alice', 'trace_id': 'req-1'},
 'user': 'alice',
 'trace_id': 'req-1'}

每个 mixin 只处理自己负责的横切逻辑,再把结果传给 MRO 中的下一个实现。


二十、什么时候组合优于多继承

如果多个组件之间没有自然的“同一调用链”关系,组合通常更清晰。

多继承要求参与者共享:

  • 方法名;
  • 参数协议;
  • 调用顺序;
  • 终止条件;
  • 异常传播约定。

如果这些条件不成立,组合更适合:

class Service:
    def __init__(self, authenticator, tracer, repository):
        self.authenticator = authenticator
        self.tracer = tracer
        self.repository = repository

    def handle(self, request):
        user = self.authenticator.authenticate(request)
        self.tracer.record(request, user)
        return self.repository.load(user)

这里的调用顺序由方法体直接表达,不依赖隐含 MRO,也不要求外部组件共同遵守 super() 协议。

选择多继承还是组合,关键不在于类的数量,而在于是否存在一个稳定、可解释的协作链。


二十一、生产代码中的验证重点

对使用协作式多继承的类,至少应验证以下内容:

def test_mro():
    assert Service.__mro__ == (
        Service,
        RetryMixin,
        LoggingMixin,
        Root,
        object,
    )


def test_each_stage_runs_once():
    events = []

    class Root:
        def process(self, **kwargs):
            events.append("Root")

    class A(Root):
        def process(self, **kwargs):
            events.append("A")
            super().process(**kwargs)

    class B(Root):
        def process(self, **kwargs):
            events.append("B")
            super().process(**kwargs)

    class C(B, A):
        pass

    C().process()
    assert events == ["B", "A", "Root"]

测试重点不是只验证最终返回值,还要验证:

  1. MRO 是否符合设计;
  2. 每个阶段是否执行;
  3. 每个阶段是否只执行一次;
  4. 参数是否正确向后传递;
  5. 某个 mixin 缺少 super() 时是否能被发现;
  6. 新增基类或交换基类顺序后,调用链是否发生了预期变化。

结语:把 MRO 看成一条受约束的调用路径

C3 线性化解决的是一个结构问题:

多继承图 -> 唯一、稳定、可验证的线性顺序

MRO 解决的是查找问题:

对象属性访问 -> 沿这条线性顺序寻找第一个匹配实现

super() 解决的是协作问题:

当前实现 -> 从当前类之后继续沿实际 MRO 查找下一个实现

三者的关系可以概括为:

C3 计算 __mro__
__mro__ 决定属性和方法查找顺序
super() 沿 __mro__ 的当前位置继续查找

因此,使用 super() 时最重要的不是把它记成“父类调用函数”,而是理解它的真实语义:

它是一个基于 MRO 的后继查找代理。

一旦掌握这一点,菱形继承、mixin、协作式 __init__、类方法中的 super()、描述符绑定以及 MRO 冲突就都可以用同一套规则解释。


系列导航与关联阅读

官方资料

本文依据 Python 官方文档、相关 PEP 与生态项目官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。