Skip to main content

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 的实现细节会补充说明对象层级如何落到 PyTypeObjectPyType_Type,但正文结论以 Python 语言语义和 CPython 当前实现边界共同支撑。

metaclass 的强度来自它站在“创建类对象”这一层。它能影响整个类族的定义时检查、属性收集、注册、实例化入口和继承冲突。这个强度也带来维护成本:一个 metaclass 修改的是类的构造规则,使用者读普通 class 语句时很容易漏掉背后的执行路径。

15.1 Metaclass and type bootstrapping

metaclass 是类对象的类型。对普通实例 utype(u) 返回实例所属的类;对类对象 Usertype(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

这段代码给出四个稳定判断。第一,uUser 的实例。第二,Usertype 的实例。第三,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 同时继承 MetaAMetaB,于是它可以作为共同子类创建 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 生成模型、根据插件配置生成代理类、测试中临时构造协议对象。核心输入仍然是 namebasesnamespace

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 里的 typePyBaseObject_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_byConstructLogMeta.__call__Account("alice") 时运行,它先让默认实例化路径创建对象,再给实例补充 constructed_by。同一个 metaclass 同时能影响类定义阶段和实例创建阶段;读代码时必须先问当前 hook 是在“定义类”还是在“调用类”。

CPython 的 dispatch 关系可以用一张图压缩。图中只画类创建和实例创建的主路径,省略 descriptor、MRO entries、class decorator 和错误处理细节。

图中的关键分叉在 MetaCallMetaCall2MetaCall 的输入是类名、基类和 namespace,输出是类对象。MetaCall2 的输入是类对象和用户传给构造调用的参数,输出是实例对象。把这两个调用混成一个阶段,会导致常见误判:以为 __call__ 控制类对象创建,或以为 __new__ 总是实例级 __new__。在 metaclass 语境中,__new__ 的接收者可以是 metaclass,返回值可以是 class object。

PyType_Type 的意义在这里收束成一个源码阅读入口。读 CPython 类型系统时,先把 type 当作一个预先构造好的 metatype 对象,再沿 type_newtype_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 是 MetaABase 已经由 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__。这里 clsChildvalue10super().__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。