Chapter 15: Metaclass System
metaclass 决定“类对象如何被创建、初始化和调用”。普通实例由类创建,类对象本身也由另一个对象创建;这个创建类对象的对象就是 metaclass。读完本章后,读者应能追踪一条 class 语句从类体执行到类对象生成的路径,并能判断自定义 metaclass 何时生效、何时产生冲突、何时应换成更小的扩展点。
本章的贯穿材料是一组“带注册表的模型类”。它把类创建、继承、动态建类和实例调用都放在同一条路径上观察。我们关注的核心问题是:当代码写下 class User(Model): ... 时,Python 到底先选谁来创建 User,再把哪些对象交给它,最后把什么对象绑定到名字 User。
Python 3.14 语言参考在 customizing class creation 中给出类创建顺序:解析 MRO entries,确定合适的 metaclass,准备 namespace,执行 class body,创建 class object。这个顺序是本章的主线。CPython 的实现细节会补充说明对象层级如何落到 PyTypeObject 和 PyType_Type,但正文结论以 Python 语言语义和 CPython 当前实现边界共同支撑。
metaclass 的强度来自它站在“创建类对象”这一层。它能影响整个类族的定义时检查、属性收集、注册、实例化入口和继承冲突。这个强度也带来维护成本:一个 metaclass 修改的是类的构造规则,使用者读普通 class 语句时很容易漏掉背后的执行路径。
15.1 Metaclass and type bootstrapping
metaclass 是类对象的类型。对普通实例 u,type(u) 返回实例所属的类;对类对象 User,type(User) 返回创建这个类对象的 metaclass。默认情况下,绝大多数用户自定义类的 metaclass 是 type。
下面的代码用最短路径观察三层对象关系:实例、类对象、metaclass。
class User:
pass
u = User()
print(type(u) is User) # True
print(type(User) is type) # True
print(type(type) is type) # True
print(type(object) is type) # True
print(type.__base__ is object) # True
这段代码给出四个稳定判断。第一,u 是 User 的实例。第二,User 是 type 的实例。第三,type 这个类对象由自身表示为 metaclass。第四,object 这个根类对象也由 type 创建。object 是大多数类继承链的根,type 是大多数类对象的创建者;这两个对象在解释器启动阶段由 CPython 预先建立,之后 Python 层代码看到的是已经闭合的对象图。
可以把这条关系写成两个方向。实例方向是 u → User → type,继承方向是 User → object,而 type 自己的继承方向是 type → object。这两条方向解决不同问题:type(x) 解释“谁创建了这个对象”,C.__mro__ 解释“属性查找沿哪些基类推进”。metaclass 系统经常难读,就是因为这两个方向会在类对象上同时出现。
类语句默认会走 type(name, bases, namespace)。Python 文档对 type() 三参数形式 的描述是:它返回一个新的 type object,近似于动态形式的 class 语句;name 成为 __name__,bases 成为 __bases__,dict 提供类体中的属性和方法。这个描述可以直接映射到类创建路径。
namespace = {
"kind": "model",
"describe": lambda self: self.kind,
}
DynamicUser = type("DynamicUser", (object,), namespace)
print(DynamicUser.__name__) # DynamicUser
print(DynamicUser.__bases__) # (<class 'object'>,)
print(DynamicUser().describe()) # model
这段动态创建代码没有写 class DynamicUser:,但结果仍然是一个类对象。namespace 中的函数对象进入类的 __dict__,之后属性访问和 descriptor binding 会把它变成实例方法。这里的关键点是:类对象创建需要三个核心输入,分别是名字、基类元组和 namespace;class 语句只是把这些输入整理出来,再交给选中的 metaclass。
这一节需要固定一个判断模型:metaclass 处理类对象的创建,类对象处理实例的创建,实例再承载业务状态。读源码或读框架代码时,看到 metaclass=...,要把注意力放在“类定义执行时发生了什么”,再观察它是否继续影响 C() 这一类实例化调用。
15.2 Custom metaclass and metaclass conflict
自定义 metaclass 通常继承 type。它可以介入 __prepare__、__new__、__init__ 和 __call__。这几个方法处在不同阶段:__prepare__ 创建类体执行所用的 namespace;__new__ 接收已经填充好的 namespace 并返回类对象;__init__ 在类对象创建后补充初始化;__call__ 在类对象被调用时控制实例创建。
贯穿材料从一个注册表 metaclass 开始。它在类定义完成时收集 table 属性,并把类对象登记到统一 registry 中。
class RegistryMeta(type):
registry = {}
@classmethod
def __prepare__(mcls, name, bases, **kwargs):
return {}
def __new__(mcls, name, bases, namespace, **kwargs):
cls = super().__new__(mcls, name, bases, namespace)
table = namespace.get("table")
if table is not None:
mcls.registry[table] = cls
return cls
class Model(metaclass=RegistryMeta):
pass
class User(Model):
table = "users"
print(type(User) is RegistryMeta) # True
print(RegistryMeta.registry["users"] is User) # True
User 的类体先把 table = "users" 写入 namespace。随后选中的 metaclass 是 RegistryMeta,所以 RegistryMeta.__new__ 接收 name="User"、bases=(Model,) 和已经填好的 namespace。注册动作发生在类定义阶段,因此模块导入时就会执行。这个时间点是 metaclass 设计的第一条边界:它适合定义时检查和定义时收集,读者需要把错误定位到类声明处,而非业务函数调用处。
metaclass 的选择遵循“候选 metaclass 中最派生者”规则。语言参考给出三类情况:没有基类也没有显式 metaclass 时使用 type;显式 metaclass 给出且它本身不是 type 的实例时直接使用它;显式 metaclass 是一个类或存在基类时,从显式 metaclass 与所有基类的 metaclass 中选出一个共同子类。没有候选对象满足共同子类条件时,类定义抛出 TypeError。
下面的代码展示冲突和修复方式。修复的核心是提供一个同时继承两个候选 metaclass 的新 metaclass。
class MetaA(type):
pass
class MetaB(type):
pass
class A(metaclass=MetaA):
pass
class B(metaclass=MetaB):
pass
# class Broken(A, B):
# pass
# TypeError: metaclass conflict
class MetaAB(MetaA, MetaB):
pass
class Combined(A, B, metaclass=MetaAB):
pass
print(type(Combined) is MetaAB) # True
Broken 失败的原因来自候选集合 {MetaA, MetaB}。MetaA 不是 MetaB 的子类,MetaB 也不是 MetaA 的子类,因此解释器无法选出一个同时覆盖二者规则的 metaclass。MetaAB 同时继承 MetaA 和 MetaB,于是它可以作为共同子类创建 Combined。
metaclass 冲突是继承层级的错误,和实例状态无关。排查顺序应固定为:先看每个基类的 type(Base),再看类头是否显式传入 metaclass=...,然后检查候选 metaclass 之间是否存在子类关系。这个顺序比直接阅读类体更有效,因为类体中的字段和方法尚未进入冲突判断的核心。
自定义 metaclass 的能力可以拆成四类。第一类是 namespace 控制,例如让类体写入有序、校验型或收集型 mapping。第二类是类对象生成前检查,例如强制字段命名规则。第三类是类对象生成后登记,例如 ORM model registry、插件 registry。第四类是实例化控制,例如单例、缓存或参数规范化。每一类都对应不同 hook;把所有逻辑堆进 __new__ 会让创建阶段和实例化阶段混在一起。
15.3 Dynamic class creation and runtime type mutation
动态建类把 class 语句拆成显式函数调用。它适合运行时才知道类名、基类或字段集合的场景,例如根据 schema 生成模型、根据插件配置生成代理类、测试中临时构造协议对象。核心输入仍然是 name、bases 和 namespace。
def build_model(name, fields):
namespace = {
"fields": tuple(fields),
"table": name.lower(),
}
return RegistryMeta(name, (Model,), namespace)
Order = build_model("Order", ["id", "total"])
print(type(Order) is RegistryMeta) # True
print(Order.fields) # ('id', 'total')
print(RegistryMeta.registry["order"] is Order) # True
这里直接调用 RegistryMeta(...),因此动态生成的 Order 仍然走同一个 metaclass。调用签名和类创建阶段一致:RegistryMeta(name, bases, namespace)。如果使用 type(name, bases, namespace),创建者会是默认 type,注册逻辑就不会执行。这个差异说明动态建类的关键不是“代码是否写成 class 语句”,而是“最终调用哪个 metaclass”。
当动态建类需要更接近 class 语句的完整流程时,可以使用标准库 types.new_class。它支持传入 metaclass 关键字,并通过回调填充 namespace。
import types
Invoice = types.new_class(
"Invoice",
(Model,),
{"metaclass": RegistryMeta},
lambda namespace: namespace.update(table="invoices"),
)
print(type(Invoice) is RegistryMeta) # True
print(RegistryMeta.registry["invoices"] is Invoice) # True
types.new_class 的优势是把 metaclass 选择、namespace 准备和类对象创建包装成稳定 API。它更适合框架内部生成类。直接调用 metaclass 更短,但调用者要自己保证参数形状、namespace 内容和 metaclass 选择。
runtime type mutation 包含两层含义。第一层是修改类对象自身,例如给类增加属性、替换方法、设置 descriptor。第二层是修改对象或类的类型关系,例如给实例改 __class__,或给类改 __bases__。第一层在用户自定义 heap class 上很常见;第二层受对象内存布局、继承关系和实现限制约束,失败时通常抛出 TypeError。
class Item:
pass
def label(self):
return "item"
Item.label = label
obj = Item()
print(obj.label()) # item
Item.label = label 修改的是类对象的属性表。之后 obj.label 触发普通属性查找和函数 descriptor binding,得到绑定到 obj 的方法。这个修改没有创建新类,也没有改变 obj.__class__。从 runtime 路径看,改变的是类对象 namespace;从调用结果看,影响了后续实例的属性访问。
对 __bases__ 和 __class__ 的修改需要更强约束。Python 语言参考在对象模型中说明对象的类型通常随对象一起固定,同时也承认在受控条件下可以修改对象类型;这类修改容易影响属性布局、slot、方法缓存和 C 扩展假设。工程上应把它看成低层动态行为,只有在代理框架、兼容层或受控迁移代码中使用,并为失败路径提供明确错误。
CPython 还维护类型相关缓存。类属性、MRO、base 和 slot 的变更会影响后续查找,解释器需要让相关缓存失效或刷新。读 Objects/typeobject.c 时,type_new、type attribute lookup cache、MRO 更新和 subclass 维护属于同一类实现线索。这里的结论不依赖具体行号:动态修改类对象会改变之后的查找结果,CPython 需要为这些变更维护一致性。
15.4 PyType_Type and metaclass dispatch
在 CPython 中,Python 层的类对象由 C 层的 PyTypeObject 表示。PyTypeObject 本身也是一种 Python 对象,它带有普通对象头,头部包含引用计数和类型指针。C API 文档在 Type Object Structures 中说明 PyType_Type 是 metatype,type object 的 ob_type 通常指向 PyType_Type。
这句话可以翻译成 Python 层关系:PyType_Type 对应 Python 里的 type,PyBaseObject_Type 对应 Python 里的 object。当你写 type(User) is RegistryMeta 时,Python 层看到的是类对象的 metaclass;当 CPython 操作这个类对象时,底层看到的是一个 PyTypeObject,其对象头和 slot 表共同决定类对象能如何被查找、调用和继承。
类创建和实例创建都走“调用一个对象”的路径,但调用目标不同。类创建阶段调用的是 metaclass:metaclass(name, bases, namespace, **kwds)。实例创建阶段调用的是类对象:User(...)。因为 User 自身的类型是 RegistryMeta,所以 User(...) 最终会进入 RegistryMeta.__call__,再由默认路径继续调用 User.__new__ 和 User.__init__。
下面的代码把这两个调用阶段分开观察。
class ConstructLogMeta(type):
def __new__(mcls, name, bases, namespace):
namespace["created_by"] = mcls.__name__
return super().__new__(mcls, name, bases, namespace)
def __call__(cls, *args, **kwargs):
obj = super().__call__(*args, **kwargs)
obj.constructed_by = type(cls).__name__
return obj
class Account(metaclass=ConstructLogMeta):
def __init__(self, name):
self.name = name
a = Account("alice")
print(Account.created_by) # ConstructLogMeta
print(a.name) # alice
print(a.constructed_by) # ConstructLogMeta
ConstructLogMeta.__new__ 在 Account 这个类对象生成时运行,它给类对象增加 created_by。ConstructLogMeta.__call__ 在 Account("alice") 时运行,它先让默认实例化路径创建对象,再给实例补充 constructed_by。同一个 metaclass 同时能影响类定义阶段和实例创建阶段;读代码时必须先问当前 hook 是在“定义类”还是在“调用类”。
CPython 的 dispatch 关系可以用一张图压缩。图中只画类创建和实例创建的主路径,省略 descriptor、MRO entries、class decorator 和错误处理细节。
图中的关键分叉在 MetaCall 和 MetaCall2。MetaCall 的输入是类名、基类和 namespace,输出是类对象。MetaCall2 的输入是类对象和用户传给构造调用的参数,输出是实例对象。把这两个调用混成一个阶段,会导致常见误判:以为 __call__ 控制类对象创建,或以为 __new__ 总是实例级 __new__。在 metaclass 语境中,__new__ 的接收者可以是 metaclass,返回值可以是 class object。
PyType_Type 的意义在这里收束成一个源码阅读入口。读 CPython 类型系统时,先把 type 当作一个预先构造好的 metatype 对象,再沿 type_new、type_call、slot 初始化、MRO 计算、attribute lookup cache 这些线索看类对象如何被创建和使用。不要试图从 Python 层递归推出 type 如何诞生;那部分属于解释器启动时建立的 C 层对象图。
15.5 Metaclass engineering boundaries
metaclass 适合处理“所有子类在定义时都必须满足同一套规则”的问题。典型场景包括:类定义时收集字段、给类族建立 registry、强制声明某些 class attribute、自动生成类级别 descriptor、统一控制实例化入口。判断是否使用 metaclass 的第一步,是确认问题发生在类对象创建阶段。
如果问题发生在类对象创建之后,优先选择更窄的扩展点。只需要修改已经创建好的类,可以使用 class decorator。只需要在子类创建后收到通知,可以使用 __init_subclass__。只需要控制单个属性的读取、写入和绑定,可以使用 descriptor。只需要生成一批相似类,可以使用普通 factory 或 types.new_class。这些替代方案的共同特点是作用范围更清楚,和第三方 metaclass 的冲突概率更低。
class BaseModel:
registry = {}
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
table = getattr(cls, "table", None)
if table is not None:
cls.registry[table] = cls
class Product(BaseModel):
table = "products"
print(BaseModel.registry["products"] is Product) # True
这段代码完成了和注册表 metaclass 相似的目标,但 hook 放在普通基类中。它适合“继承这个基类的子类都要登记”这一类需求。它处理不了 namespace 准备阶段,也处理不了多个基类 metaclass 的合并规则;相应地,它的阅读成本和冲突成本也更低。
工程判断可以按以下顺序执行。先确认需求是否发生在 class definition 阶段。再确认是否需要控制 namespace 准备或类对象创建。然后检查项目中已有基类的 metaclass,特别是 ABCMeta、ORM base、Enum 类族和框架基类。接着评估错误出现时间:类定义时报错适合强约束,业务调用时报错适合普通函数或实例方法。最后确定替代方案是否已经覆盖需求。
metaclass 的主要风险有三个。第一,类声明看起来普通,实际执行了额外逻辑,阅读者需要跳到 metaclass 才能解释结果。第二,多继承会把多个 metaclass 的兼容性问题暴露出来,冲突位置通常在导入或类定义阶段。第三,__call__、__new__、descriptor、__init_subclass__ 同时存在时,创建阶段、绑定阶段和实例化阶段容易交叉,调试时需要重新拆阶段。
本章的收束判断是:metaclass 是类型系统中创建类对象的入口。它能统一控制一个类族,但应服务于类定义阶段的结构性规则。读到 metaclass 代码时,先定位候选 metaclass,再追踪 namespace、class object 和 instance object 的流向,最后判断这个设计是否可以收缩到 decorator、__init_subclass__、descriptor 或 factory。
最小自检任务
阅读下面的代码,判断 Child 的 metaclass 是什么,marker 在哪个阶段写入,Child(10) 为什么会得到带 value 的实例。
class MetaA(type):
def __new__(mcls, name, bases, namespace):
namespace["marker"] = name.lower()
return super().__new__(mcls, name, bases, namespace)
def __call__(cls, value):
obj = super().__call__()
obj.value = value
return obj
class Base(metaclass=MetaA):
pass
class Child(Base):
pass
c = Child(10)
答案要点
Child 的 metaclass 是 MetaA。Base 已经由 MetaA 创建,Child(Base) 的候选 metaclass 来自 type(Base),所以解释器继续选择 MetaA 创建 Child。
marker 在类对象创建阶段写入。Child 的类体执行后形成 namespace,随后 MetaA.__new__ 接收 name="Child"、bases=(Base,) 和 namespace,并在调用 type.__new__ 前写入 namespace["marker"] = "child"。因此 Child.marker 来自类对象的属性表。
Child(10) 进入实例创建阶段。由于 type(Child) is MetaA,调用 Child(10) 会进入 MetaA.__call__。这里 cls 是 Child,value 是 10。super().__call__() 创建一个 Child 实例,然后 metaclass 的 __call__ 给这个实例写入 value,最后返回实例对象。
这段代码的检查顺序是:先看基类的 metaclass,再看类创建阶段的 __new__,最后看实例化阶段的 __call__。marker 属于类对象,value 属于实例对象,二者对应不同阶段。
本章知识点总结
- 元类定位:metaclass 是类对象的类型,负责创建和初始化类对象。
- 默认创建者:普通用户自定义类默认由
type创建,type(User) is type表达这个关系。 - 双重方向:
type(x)说明对象创建关系,__mro__说明继承查找关系。 - 类创建输入:类对象创建的核心输入是类名、基类元组和 namespace。
- 创建顺序:类定义会依次确定 metaclass、准备 namespace、执行类体、调用 metaclass 创建类对象。
- 自定义入口:
__prepare__、__new__、__init__和__call__分别介入不同阶段。 - 冲突条件:多个候选 metaclass 需要存在共同子类,否则类定义会抛出
TypeError。 - 动态建类:
type(name, bases, namespace)或显式 metaclass 调用可以在运行时创建类对象。 - 运行时修改:修改类对象属性会影响后续属性查找,修改类型关系受内存布局和实现约束。
- CPython 对应:Python 层的
type对应 C 层PyType_Type,类对象由PyTypeObject表示。 - 调用分层:类创建阶段调用 metaclass,实例创建阶段调用类对象并进入 metaclass 的
__call__。 - 工程边界:metaclass 适合类定义阶段的结构性规则,较窄需求应优先考虑 decorator、
__init_subclass__、descriptor 或 factory。