本节目标:从 CPython 实现角度讲清
class语句背后发生了什么,以及元类钩子与__init_subclass__的精确执行时序。
适用版本:Python 3.12+(实测 3.14.6)
2.1 元类与 __init_subclass__
站内专题 Python 元编程与动态特性深度解析
已经讲过元类「怎么用」:type() 三参数建类、单例元类、自动注册元类、元类冲突的解决办法。这一节不再重复用法,而是往下挖一层——class 语句在 CPython 里到底翻译成了什么,以及那些钩子函数在毫秒级别上的真实调用顺序。这些顺序靠读文档是记不准的,只能靠实测。
2.1.1 class 语句脱糖成了什么
先看一段最普通的类定义,用 dis 把它编译后的字节码打印出来:
import dis
def make():
class C:
x = 1
def f(self):
return 1
return C
dis.dis(make)
真实输出(Python 3.14.6,节选):
5 LOAD_BUILD_CLASS
PUSH_NULL
LOAD_CONST 0 (<code object C ...>)
MAKE_FUNCTION
LOAD_CONST 1 ('C')
CALL 2
STORE_FAST 0 (C)
关键信息全在这里:class 语句并不是一条「原子指令」,它编译成了一个对内置函数 __build_class__ 的调用。参数是两个:一个是类体的代码对象(MAKE_FUNCTION 把它包成函数),另一个是类名字符串 'C'。
也就是说,class C: 大致等价于:
def __class_body__():
x = 1
def f(self):
return 1
return locals()
C = __build_class__(__class_body__, "C")
类体是一段独立编译的代码对象——这是理解后面所有钩子时序的前提。类体里的语句在「类对象存在之前」就已经执行完了,它的产物是一个普通的命名空间字典。
再看类体自身的字节码(同一个 dis.dis 输出的后半段):
-- MAKE_CELL 0 (__classdict__)
5 RESUME 0
LOAD_NAME 0 (__name__)
STORE_NAME 1 (__module__)
LOAD_CONST 0 ('make.<locals>.C')
STORE_NAME 2 (__qualname__)
LOAD_SMALL_INT 5
STORE_NAME 3 (__firstlineno__)
LOAD_LOCALS
STORE_DEREF 0 (__classdict__)
6 LOAD_SMALL_INT 1
STORE_NAME 4 (x)
7 LOAD_CONST 1 (<code object f ...>)
MAKE_FUNCTION
STORE_NAME 5 (f)
LOAD_CONST 2 (())
STORE_NAME 6 (__static_attributes__)
LOAD_FAST_BORROW 0 (__classdict__)
STORE_NAME 7 (__classdictcell__)
编译器往命名空间里自动塞了几个键:__module__、__qualname__、__firstlineno__、__static_attributes__,以及 3.14 特有的 __classdictcell__。这就是为什么上一节 2.1.2 的实验里,元类 __new__ 收到的 namespace 里有你没写的键。其中 __firstlineno__(类定义起始行号)和 __static_attributes__(类体函数中通过 self.X 访问过的属性名)都是 3.13 才引入的数据模型改进。
2.1.2 元类钩子的精确时序
文档只会笼统地说「元类可以介入类的创建」。到底谁先谁后?下面这段代码把每个钩子都插了打印,跑一次就清楚了:
class Meta(type):
@classmethod
def __prepare__(mcs, name, bases, **kw):
print("1. __prepare__", name)
return {}
def __new__(mcs, name, bases, ns, **kw):
print("3. __new__ 收到 namespace keys:", list(ns))
cls = super().__new__(mcs, name, bases, ns)
print("4. __new__ 返回类对象", cls)
return cls
def __init__(cls, name, bases, ns, **kw):
print("6. __init__", cls)
super().__init__(name, bases, ns)
class Desc:
def __set_name__(self, owner, name):
print("5a. __set_name__", owner.__name__, name)
class Base:
def __init_subclass__(cls, **kw):
print("5b. __init_subclass__ 父类收到子类", cls.__name__)
class C(Base, metaclass=Meta):
print("2. 类体执行")
x = 1
d = Desc()
真实输出:
1. __prepare__ C
2. 类体执行
3. __new__ 收到 namespace keys: ['__module__', '__qualname__', '__firstlineno__', 'x', 'd', '__static_attributes__']
5a. __set_name__ C d
5b. __init_subclass__ 父类收到子类 C
4. __new__ 返回类对象 <class '__main__.C'>
6. __init__ <class '__main__.C'>
完整顺序是:
| 序 | 阶段 | 说明 |
|---|---|---|
| 1 | __prepare__ | 元类类方法,返回类体要写入的命名空间映射 |
| 2 | 类体执行 | 所有类体语句在此运行,结果写进 __prepare__ 返回的映射 |
| 3 | 元类 __new__ | 开始构造类对象 |
| 4 | __set_name__ | 在 super().__new__ 内部对每个描述符调用 |
| 5 | __init_subclass__ | 在 super().__new__ 内部,对父类调用 |
| 6 | 元类 __init__ | 元类 __new__ 返回后调用 |
两个反直觉的点:
__set_name__和__init_subclass__都发生在__new__内部(第 4、5 步夹在第 3 步和第 4 步的返回之间),而不是在__init__之后。因为它们在 CPython 里是由type.__new__亲自触发的。__set_name__先于__init_subclass__。所以如果父类的__init_subclass__想读取子类描述符已绑定的名字,此时已经就绪。
2.1.3 type() 三参数:同一套机制的裸调用
class 语句最终走的是 type.__call__(mcls, name, bases, ns) → mcls.__new__ → mcls.__init__。那么直接用 type(name, bases, ns) 建类,会不会漏掉 __set_name__ 和 __init_subclass__?
class Desc:
def __set_name__(self, owner, name):
print(" __set_name__ 触发:", owner.__name__, name)
class Base:
def __init_subclass__(cls, **kw):
print(" __init_subclass__ 触发:", cls.__name__)
T = type('T', (Base,), {'d': Desc()})
print("type(T) =", type(T).__name__)
真实输出:
__set_name__ 触发: T d
__init_subclass__ 触发: T
type(T) = type
结论:type() 三参数照样触发这两个钩子,因为它调用的就是同一个 type.__new__。它也不会漏掉元类推导——如果基类的元类是 Meta,用 type('T2', (A,), {}) 建出的类,type(T2) 同样是 Meta。
type() 三参数唯一绕过的是 __prepare__:它直接接收一个现成的字典,不经过 __prepare__。所以「需要自定义命名空间映射」是少数必须走 class 语句 + 元类的场景。
那用 class 语句比 type() 贵多少?实测 20 万次建一个最小类:
type() 三参数: 3.982 µs/次
class 语句 : 4.089 µs/次
比值: 1.03x
差距只有 3%——class 语句本身就是 __build_class__ 对 type() 的一层薄封装。「用 type() 动态建类更快」是个流传很广的误解,两者开销几乎相同。
2.1.4 __prepare__ 真正的用途
__prepare__ 的返回值就是类体执行时用的命名空间。默认返回一个普通 dict,你可以换成任何映射对象。一个实际用途是保留成员的定义顺序——虽然普通 dict 从 3.7 起也保序,但你可以借此记录一份「只属于本类、不含自动注入键」的清单:
import collections
class OrderedMeta(type):
@classmethod
def __prepare__(mcs, name, bases, **kw):
return collections.OrderedDict()
def __new__(mcs, name, bases, ns, **kw):
cls = super().__new__(mcs, name, bases, dict(ns))
cls._defined = [k for k in ns if not k.startswith('__')]
return cls
class Ordered(metaclass=OrderedMeta):
z = 1
a = 2
m = 3
print("定义顺序:", Ordered._defined)
真实输出:
定义顺序: ['z', 'a', 'm']
注意 dict(ns) 这一步:因为 __new__ 传给 type.__new__ 的命名空间会被存成类的 __dict__,而 CPython 期望它是真正的 dict,所以最好在交给父类前转换一次。定义顺序按源码书写顺序(z, a, m),不是字母序——这正是 dataclasses、ORM 字段排序依赖的机制。
2.1.5 元类冲突的本质是 MRO
当两个基类用不同元类时,Python 报:
metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
这条规则可以用一句话概括:子类的元类,必须是所有基类元类的(非严格)子类。所以解决方式不是「随便选一个」,而是造一个同时继承自两者的元类:
class MetaA(type): pass
class MetaB(type): pass
class A(metaclass=MetaA): pass
class B(metaclass=MetaB): pass
class MetaAB(MetaA, MetaB): pass
class C2(A, B, metaclass=MetaAB): pass
print("type(C2) =", type(C2).__name__) # MetaAB
print("MetaAB.__mro__ =", [c.__name__ for c in MetaAB.__mro__])
真实输出:
解决后 type(C2) = MetaAB
MetaAB.__mro__ = ['MetaAB', 'MetaA', 'MetaB', 'type', 'object']
这解释了为什么框架里的元类总是尽量继承 type 并保持浅层:每多一个不相关的元类,用户组合多个 mixin 基类时就越容易撞上冲突。
2.1.6 __init_subclass__:大多数时候不需要元类
专题里的「自动注册插件」示例用了元类。但同样的需求,__init_subclass__ 就能完成,而且更轻:
class BaseSub:
registry = {}
def __init_subclass__(cls, **kw):
super().__init_subclass__(**kw)
BaseSub.registry[cls.__name__] = cls
class Q1(BaseSub): pass
class Q2(BaseSub): pass
print("__init_subclass__ 注册表:", sorted(BaseSub.registry))
真实输出:
__init_subclass__ 注册表: ['Q1', 'Q2']
__init_subclass__ 有几个容易忽略的语义:
- 它隐式是一个
classmethod,定义时不需要@classmethod,第一个参数就是新创建的子类。 - 它从父类一侧被调用:
Base.__init_subclass__(Sub)。 - 定义在
class语句上的关键字参数会透传进来:class Sub(Base, tag="a")会让__init_subclass__(cls, tag="a")收到tag。多级继承时记得super().__init_subclass__(**kw)把参数继续往上传递。
选型规则:
| 需求 | 用 __init_subclass__ | 用元类 |
|---|---|---|
| 子类创建后注册/校验/改写类属性 | ✅ | 可以但过重 |
| 修改本类自身的创建过程(命名空间、类对象) | ❌ | ✅ |
需要自定义命名空间映射(__prepare__) | ❌ | ✅ |
想拦截实例的创建(如单例,重写 __call__) | ❌ | ✅ |
| 不引入额外元类、避免组合冲突 | ✅ | ❌ |
一句话:__init_subclass__ 处理「子类建成之后」,元类处理「类本身怎么建成」。能用前者就别上后者,元类每多一个都会增加下游使用者遇到 metaclass conflict 的概率。
2.1.7 3.13+ 的类命名空间新键
实测顺带确认了两个 3.13 引入的类属性,它们直接来自类体字节码的自动注入:
class P:
def __init__(self):
self.x = 1
self.y = 2
print("__static_attributes__ =", P.__static_attributes__)
真实输出:
__static_attributes__ = ('x', 'y')
__static_attributes__:类中所有函数通过self.X访问过的属性名元组,按字母序。它是给解释器优化(尤其自适应/JIT)用的,能预先知道实例可能有哪些属性。__firstlineno__:类定义的起始行号,方便工具报错定位。
这两个键是版本差异:3.12 及更早的类命名空间里没有它们。如果你的元类 __new__ 遍历命名空间做处理,需要把这两个键也纳入考虑(比如 2.1.4 里用 startswith('__') 过滤时刚好能排除掉)。
小结
class语句编译成对__build_class__的调用,类体是一段独立代码对象;type()三参数走的是同一套type.__new__。- 元类钩子的精确顺序:
__prepare__→ 类体 →__new__→(内部)__set_name__→(内部)__init_subclass__→__init__。 type()三参数同样触发__set_name__与__init_subclass__,也会做元类推导;唯一绕过的是__prepare__。class语句与type()建类性能几乎相同(实测 1.03x),「动态建类更快」是误解。- 元类冲突的本质是「子类元类必须是基类元类的子类」;
__init_subclass__能覆盖大多数注册/校验需求,应优先使用。 __static_attributes__/__firstlineno__是 3.13 新增的类命名空间键。
理解了「类怎么被造出来」,下一步就是「类造好之后还能怎么改」。下一节我们看类装饰器、__set_name__ 与属性工厂——它们恰好都发生在 2.1.2 那张时序表的第 4 步之后。
阅读导航:上一节:1.3 魔术方法驱动的协议设计 · 下一节:2.2 类装饰器、set_name 与属性工厂 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。