Skip to main content

Chapter 14: Inheritance and MRO

继承把多个类的属性、方法和协议实现放到同一个对象查找路径里。读本章之后,读者应能拿到一个类定义,定位它的 __mro__,判断属性和方法会落到哪一个类,解释 super() 的下一站,识别 mixin 和 descriptor 叠加时的查找边界,并在 metaclass 冲突出现时给出判断步骤。

本章使用一个贯穿例子。Service 继承两个 mixin,两个 mixin 又共享同一个根类。这个结构能覆盖单继承、多继承、菱形继承、协作式调用、descriptor 和 metaclass 选择几个关键问题。

class Root:
def handle(self, request):
return ["root"]

class AuthMixin(Root):
def handle(self, request):
result = super().handle(request)
result.append("auth")
return result

class AuditMixin(Root):
def handle(self, request):
result = super().handle(request)
result.append("audit")
return result

class Service(AuthMixin, AuditMixin):
pass

在 CPython 中,类对象创建完成后会持有 __mro__。这个元组决定 Service().handle(...) 的查找顺序,也决定 AuthMixin.handle 内部 super().handle(...) 的下一段路径。Python 官方文档把 MRO 相关规则集中在 Python 3 MRO HOWTObuiltins.superclass creation data model 中;本章把这些规则整理成可以直接读代码的判断模型。

14.1 Inheritance model and MRO

继承模型解决的问题是:当一个对象所在的类没有直接给出目标属性时,解释器沿哪一条类序列继续查找。这个类序列就是 method resolution order,通常写作 MRO。MRO 的名称里有 method,但它覆盖的是属性查找;方法是函数对象经过 descriptor 绑定之后形成的调用入口。

Service 来说,继承声明提供了两个输入:当前类自身和 bases 元组。bases 元组按源码顺序写成 (AuthMixin, AuditMixin)。解释器创建 Service 时,会把当前类、两个直接基类、间接基类和 object 合并成一个线性序列。这个序列保存在类对象的 __mro__ 上。

print(Service.__mro__)
# (<class '__main__.Service'>,
# <class '__main__.AuthMixin'>,
# <class '__main__.AuditMixin'>,
# <class '__main__.Root'>,
# <class 'object'>)

这个输出说明三个事实。第一,当前类永远排在最前面,因为对象查找要先给最具体的类一次定义机会。第二,直接基类的局部顺序被保留,AuthMixin 排在 AuditMixin 前面。第三,公共祖先 Root 只出现一次,两个 mixin 都能共享同一个收束点。

属性读取会使用这个序列。表达式 Service().handle 先看实例所属类 Service,再看 AuthMixin,随后才看 AuditMixinRoot。由于 AuthMixin 定义了 handle,查找阶段在这里拿到函数对象;函数对象实现 descriptor 协议,所以它会绑定到实例,形成 bound method。调用 bound method 时,self 指向原始的 Service 实例。

service = Service()
method = service.handle
print(method.__self__ is service)
print(method.__func__ is AuthMixin.handle)

这个例子给出继承阅读的第一条判断顺序:先看实例的实际类,再看该类的 __mro__,再看目标名字在哪一个类的命名空间里第一次被命中,最后再判断命中的对象是否通过 descriptor 改变返回值。Service 没有定义 handle,所以查找落到 AuthMixin.handle;绑定发生在函数对象上,调用时的 self 仍然是 Service 实例。

这条路径也解释了继承和覆盖的关系。子类在自己的 class dict 中写入同名属性时,MRO 的第一个命中点就变成子类;子类没有写入同名属性时,解释器继续沿 MRO 找到更后面的类。覆盖关系来自查找顺序,继承声明只是生成查找顺序的输入。

class FastService(AuthMixin, AuditMixin):
def handle(self, request):
return ["fast"]

print(FastService.__mro__)
print(FastService().handle({}))

FastService.handle 直接排在 MRO 的第一个命中点。它没有调用 super(),所以后面的 mixin 和根类在这次调用中没有执行。这里的工程结论是:MRO 决定可达路径,方法体内部是否继续调用下一站决定执行链是否走完整。

CPython 源码阅读时,类型对象相关实现集中在 CPYTHON_SRC=/path/to/cpython/Objects/typeobject.c。读 MRO 时可以搜索 type_mro_implmro_implementationpmergemro_internal;读 super() 时可以搜索 supercheckdo_super_lookupsuper_getattro。这些入口分别对应用户可见的 type.mro()、类创建时的 MRO 计算、C3 合并和 super 代理查找。

14.2 C3 linearization and diamond inheritance

C3 linearization 解决的问题是:多个基类同时给出顺序约束时,解释器如何生成一条稳定、单调、保留局部优先级的类序列。菱形继承让这个问题变得可见,因为两个分支会共享同一个祖先。

贯穿例子的继承图可以写成下面的形状。图中箭头表示“子类继承父类”,实际查找顺序由 C3 合并结果决定。

C3 的输入包含每个直接基类自己的 MRO,以及当前类声明中的直接基类列表。对 Service(AuthMixin, AuditMixin) 来说,输入可以写成:

AuthMixin.__mro__
# (AuthMixin, Root, object)

AuditMixin.__mro__
# (AuditMixin, Root, object)

Service.__bases__
# (AuthMixin, AuditMixin)

C3 合并会反复选择一个“合格头部”。一个候选类位于某个待合并列表的头部,并且没有出现在其他待合并列表的尾部时,它就可以进入结果。这个规则能保留直接基类顺序,也能让公共祖先延后到所有更具体的分支之后。

Service 的合并过程可以压缩表示为:

L[Service] = Service + merge(
[AuthMixin, Root, object],
[AuditMixin, Root, object],
[AuthMixin, AuditMixin],
)

# 第一次选择 AuthMixin
# 第二次选择 AuditMixin
# 第三次选择 Root
# 第四次选择 object

第一次选择 AuthMixin,因为它是第一个列表的头部,也没有出现在其他列表的尾部。选择后从所有列表移除它。第二次选择 AuditMixin,理由相同。Root 在一开始排在 AuthMixinAuditMixin 的后面,所以它要等两个更具体的分支进入结果后才能进入。最终得到 Service, AuthMixin, AuditMixin, Root, object

局部优先级是 C3 的第一条关键约束。class Service(AuthMixin, AuditMixin) 的括号顺序表达作者意图:在两个分支都能提供同名属性时,AuthMixin 先于 AuditMixin 被考虑。C3 会保留这种局部顺序,除非其它约束让整个结构无法线性化。

单调性是第二条关键约束。某个基类序列中已经确立的先后关系,在子类的 MRO 中保持一致。这个性质让继承层级可以继续扩展:新增子类不会反向改变已有父类内部的解析顺序。没有这个性质时,给类增加一层子类可能改变旧方法之间的胜出关系,调试会变成对整棵继承树的猜测。

冲突结构会在类创建阶段失败。下面的例子同时要求 X 先于 Y,又要求 Y 先于 X。C3 找不到满足所有约束的合格头部,因此类对象无法创建。

class X:
pass

class Y:
pass

class A(X, Y):
pass

class B(Y, X):
pass

# class C(A, B):
# pass
# TypeError: Cannot create a consistent method resolution order (MRO)

这里的失败发生在 class statement 执行期间。C 的类对象没有成功生成,后续也没有 C.__mro__ 可以检查。排查这类问题时,先写出每个直接基类的 __mro__,再把直接基类列表也加入待合并输入;如果某个候选头部总是出现在另一个列表的尾部,冲突点就在这几个类之间。

C3 的工程含义可以用一句话收束:继承声明提供局部顺序,父类 MRO 提供已有约束,C3 负责把这些约束合并成一条全局查找路径。能够合并时,类创建成功;约束互相冲突时,类创建阶段给出 TypeError

14.3 super and cooperative inheritance

super() 解决的问题是:当前方法已经位于 MRO 的某一站,后续调用应从哪一站继续查找同名属性。它返回一个代理对象,代理对象保存两个核心信息:起点类和绑定对象。零参数 super() 在普通实例方法中会使用当前方法所在的类作为起点类,使用第一个形参对应的对象作为绑定对象。

AuthMixin.handle 内部调用 super().handle(request) 时,起点类是 AuthMixin,绑定对象是 Service 实例。解释器查看绑定对象实际类的 MRO:Service, AuthMixin, AuditMixin, Root, object。起点类 AuthMixin 之后的下一站是 AuditMixin,所以这次调用进入 AuditMixin.handle

print(Service().handle({}))
# ['root', 'audit', 'auth']

执行顺序和返回列表的顺序相反。调用先进入 AuthMixin.handle,随后通过 super() 进入 AuditMixin.handle,再进入 Root.handleRoot.handle 返回 ['root'] 后,控制权回到 AuditMixin.handle 追加 audit,最后回到 AuthMixin.handle 追加 auth

这个图只描述一次调用链。关键点在于 super() 的下一站由“绑定对象的 MRO”和“当前起点类”共同决定。它不会简单等同于源码中写出的某个父类名称。AuthMixin 的直接基类是 Root,但在 Service 的 MRO 中,AuthMixin 后面紧跟 AuditMixin;因此 AuthMixin.handle 里的 super() 会先进入 AuditMixin.handle

协作式继承要求同一个方法族遵守一致的调用约定。每一站接收兼容的参数,完成自己负责的局部工作,并把调用继续交给 super()。最末端的根类提供收束实现。这样多个 mixin 才能按 MRO 串成完整管线。

class Root:
def handle(self, request):
return []

class AuthMixin(Root):
def handle(self, request):
request["checked"] = True
return super().handle(request) + ["auth"]

class AuditMixin(Root):
def handle(self, request):
request["audited"] = True
return super().handle(request) + ["audit"]

class Service(AuthMixin, AuditMixin):
pass

这段代码的重点是调用约定。三个 handle 都接收 request,都返回 list,两个 mixin 都继续调用下一站。只要某一站改成不兼容签名,或者中途直接返回而不继续委派,后续类的实现就会失去执行机会。

class StopMixin(Root):
def handle(self, request):
return ["stop"]

class StoppedService(AuthMixin, StopMixin, AuditMixin):
pass

print(StoppedService.__mro__)
print(StoppedService().handle({}))

StoppedService 的 MRO 中仍然包含 AuditMixin,但 StopMixin.handle 没有继续委派,执行链会在这里收束。这个例子区分了两件事:MRO 表示查找可达性,协作式调用表示运行时是否继续走向下一站。读多继承代码时,需要同时检查这两层。

零参数 super() 还依赖编译器为方法体提供的 __class__ cell 和第一个局部变量。它适合普通方法和 classmethod 的直接方法体。嵌套函数、生成器表达式或手动改变第一个参数含义时,应使用显式形式 super(CurrentClass, self) 或重新整理代码结构。这个边界来自 super() 对当前 frame 和绑定对象的依赖。

14.4 Mixin design and descriptor interaction

Mixin 设计解决的问题是:把一段可组合行为放进继承链,让业务类通过 MRO 获得这段行为。Mixin 的能力来自普通类机制,它没有单独的 runtime 类型。一个类写在 bases 元组里,就参与 MRO、descriptor 查找、super() 委派和 metaclass 选择。

良好的 mixin 应把自己当成 MRO 中的一站。它提供一小段行为,遵守协作式调用约定,并把状态依赖压缩到清晰的属性或协议上。下面的例子把认证和审计拆成两个 mixin,业务类只负责组合顺序。

class AuthMixin:
def handle(self, request):
request["user"] = "alice"
return super().handle(request) + ["auth"]

class AuditMixin:
def handle(self, request):
request["audit_id"] = 42
return super().handle(request) + ["audit"]

class Root:
def handle(self, request):
return ["root"]

class Service(AuthMixin, AuditMixin, Root):
pass

这个版本把 Root 放到最终业务类的基类列表中。AuthMixinAuditMixin 自身不继承 Root,所以它们更像可插拔步骤。Service.__mro__ 仍然给出唯一执行顺序:Service, AuthMixin, AuditMixin, Root, object。当项目中有多个业务类复用同一组 mixin 时,这种写法能让收束类由业务类统一决定。

Mixin 和 descriptor 叠加时,属性查找先由 MRO 找到类命名空间中的候选对象,再由 descriptor 协议决定返回值。data descriptor 定义 __set____delete__,读取时优先级高于实例字典。non-data descriptor 只定义 __get__,实例字典可以覆盖它。函数对象属于 non-data descriptor,普通方法绑定就发生在这里。

class Setting:
def __get__(self, instance, owner):
if instance is None:
return self
return f"setting for {owner.__name__}"

class ConfigMixin:
config = Setting()

class Service(ConfigMixin):
pass

print(Service().config)
# setting for Service

Service().config 的查找路径先沿 Service.__mro__ 找到 ConfigMixin.config,命中的对象是 Setting 实例。随后解释器调用 Setting.__get__(instance, owner)。这里的 ownerService,所以 descriptor 能感知最终业务类。descriptor 的绑定阶段让 mixin 中定义的属性可以根据实际子类调整行为。

如果多个 mixin 定义同名 descriptor,MRO 决定哪一个 descriptor 先被命中。下面的例子中,LeftConfig.config 排在 RightConfig.config 前面,读取 config 时只触发左侧 descriptor。

class NamedSetting:
def __init__(self, name):
self.name = name

def __get__(self, instance, owner):
return self.name

class LeftConfig:
config = NamedSetting("left")

class RightConfig:
config = NamedSetting("right")

class Service(LeftConfig, RightConfig):
pass

print(Service().config)
# left

这个例子给出 mixin 组合的第二条判断顺序:先检查业务类的 __mro__,定位同名属性的第一个命中类;再检查命中对象是否为 data descriptor、non-data descriptor 或普通值;最后判断实例字典是否有机会覆盖它。继承顺序和 descriptor 优先级属于两个层级,排查时要分开处理。

Mixin 设计的失败通常来自三类边界。第一类是同名方法或同名 descriptor 没有明确优先级,组合顺序改变就改变行为。第二类是某个 mixin 在协作式方法中中断 super() 链,让后续步骤失效。第三类是 mixin 隐式依赖实例上某些属性,却没有在协议中说明获取点和写入点。稳定的 mixin 应把这些依赖写成明确方法、descriptor 或初始化约定。

14.5 Metaclass interaction

Metaclass interaction 解决的问题是:类对象本身由哪一个类来创建。普通实例由 class 创建,class 对象由 metaclass 创建。继承体系中每个基类都有自己的 metaclass,当前类也可能显式写出 metaclass=。解释器必须先选出一个能兼容所有候选项的 metaclass,然后才能准备 namespace、执行 class body、创建类对象并计算 MRO。

最常见的情况是所有基类都由 type 创建,当前类也没有显式指定 metaclass。此时新类的 metaclass 就是 type

class Base:
pass

class Service(Base):
pass

print(type(Service) is type)

自定义 metaclass 会进入候选集合。解释器会选择“最派生”的 metaclass,也就是一个同时是所有候选 metaclass 子类型的类。如果找不到这样的类,class statement 失败并抛出 TypeError。这个规则保证新类的 metaclass 能覆盖所有基类对类对象创建、namespace 准备和类型级行为的要求。

class MetaA(type):
pass

class MetaB(type):
pass

class A(metaclass=MetaA):
pass

class B(metaclass=MetaB):
pass

# class C(A, B):
# pass
# TypeError: metaclass conflict

A 需要 MetaAB 需要 MetaBMetaAMetaB 互相没有子类型关系,所以解释器没有一个共同的最派生 metaclass 可以选择。解决方式是显式定义一个同时继承二者的 metaclass,并让当前类使用它。

class MetaAB(MetaA, MetaB):
pass

class C(A, B, metaclass=MetaAB):
pass

print(type(C) is MetaAB)
print(C.__mro__)

这里有两条 MRO 需要区分。C.__mro__ 是实例属性查找用的类序列,描述 C 的对象在 AB 和更上层类之间如何查找属性。MetaAB.__mro__ 是类对象自身进行类型级属性查找时使用的序列,描述 C.some_class_level_name 在 metaclass 侧如何解析。实例层 MRO 和 metaclass 层 MRO 都是 MRO,但它们作用在不同对象上。

Metaclass 选择发生在类对象创建流程中,比普通实例查找更早。Python data model 描述的顺序可以整理为:解析非 type 基类的 MRO entries,确定合适的 metaclass,准备 class namespace,执行 class body,创建 class object。MRO 计算属于创建 class object 过程的一部分。这个顺序解释了一个常见现象:metaclass 冲突会阻止类对象创建,因此实例层 MRO 根本没有机会生成。

继承体系中同时出现 __mro_entries__、自定义 metaclass 和多继承时,可以按下面的顺序排查:先看 class statement 原始 bases,再确认非 type 基类是否通过 __mro_entries__ 替换成真正基类;随后收集显式 metaclass 和所有基类的 type(base);接着判断是否存在一个候选 metaclass 是所有候选项的子类型;最后再进入 C3 MRO 合并。这样可以把“基类替换问题”“metaclass 选择问题”和“实例层 MRO 冲突问题”分开定位。

本章最终建立的判断模型是:继承声明不是直接调用关系,它先参与类对象创建,生成 __mro__ 和 metaclass 关系;属性访问再沿 __mro__ 定位候选对象;descriptor 和 super() 在这个候选路径上继续改变绑定和委派。读多继承代码时,先固定 __mro__,再看 descriptor 绑定和方法体里的 super(),最后检查 metaclass 兼容性。

最小自检任务

阅读下面代码,判断 App().run() 的返回值,并说明每一步由哪一个 runtime 规则决定。

class Root:
def run(self):
return ["root"]

class CacheMixin(Root):
def run(self):
result = super().run()
result.append("cache")
return result

class LogMixin(Root):
def run(self):
result = super().run()
result.append("log")
return result

class NameDescriptor:
def __get__(self, instance, owner):
return owner.__name__

class NamedMixin:
name = NameDescriptor()

class App(CacheMixin, LogMixin, NamedMixin):
pass

同时回答两个边界问题:App().name 返回什么;如果 LogMixin.run 改成直接 return ["log"]App().run() 的执行链如何变化。

答案要点

App.__mro__App, CacheMixin, LogMixin, Root, NamedMixin, object。C3 合并会先保留直接基类顺序,选择 CacheMixinLogMixin,再放入二者共享的 RootNamedMixin 没有参与 run 的同名方法链,但它仍然在 MRO 中。

App().run() 首先命中 CacheMixin.run。这个函数通过 super()CacheMixin 后面继续查找,所以进入 LogMixin.runLogMixin.run 再通过 super() 进入 Root.run。返回值先由 Root 生成 ['root'],回到 LogMixin 后追加 log,回到 CacheMixin 后追加 cache,最终结果是 ['root', 'log', 'cache']

App().name 沿 MRO 找到 NamedMixin.name。命中的对象实现 __get__,所以读取结果来自 descriptor 绑定,ownerApp,返回值是 'App'

如果 LogMixin.run 直接返回 ['log'],执行链在 LogMixin 收束,Root.run 不会执行。CacheMixin.run 仍然会在返回后追加 cache,最终结果是 ['log', 'cache']。这里的判断同时使用两层规则:MRO 表示下一站可达,方法体是否调用 super() 决定运行时是否继续委派。

本章知识点总结

  • MRO 序列__mro__ 是类对象上的线性查找序列,属性和方法查找都会沿这条序列定位第一个命中点。
  • 继承输入:class statement 中的 bases 元组提供局部顺序,父类已有 MRO 提供继承约束。
  • C3 合并:C3 通过选择合格头部合并多个 MRO 列表,结果同时保留局部顺序和单调性。
  • 菱形收束:多个分支共享同一祖先时,公共祖先在最终 MRO 中只保留一次,并排在更具体分支之后。
  • 冲突失败:当基类顺序约束互相矛盾时,类对象创建阶段会抛出 TypeError,后续没有可用的 __mro__
  • super 代理super() 根据绑定对象的 MRO 和当前起点类确定下一站,适合协作式多继承。
  • 协作调用:多继承方法链需要各站保持兼容签名、局部处理和继续委派,根类负责提供收束实现。
  • Mixin 组合:Mixin 作为普通基类参与 MRO,它的行为稳定性依赖明确顺序、清晰协议和可检查状态依赖。
  • Descriptor 绑定:MRO 先定位 descriptor 对象,随后 descriptor 协议决定读取结果、绑定对象和覆盖边界。
  • 同名属性:多个 mixin 定义同名属性时,第一个 MRO 命中点决定候选对象,descriptor 类型再决定实例属性是否能覆盖。
  • Metaclass 选择:类创建前会从显式 metaclass 和所有基类 metaclass 中选择最派生 metaclass,候选不兼容时创建失败。
  • 双层 MRO:实例层 C.__mro__ 处理实例属性查找,metaclass 层 MRO 处理类对象自身的类型级属性查找。
  • 排查顺序:读继承问题时先固定 __mro__,再看 descriptor 绑定和 super() 委派,最后检查 metaclass 兼容性。