Python 系列 · 第六篇

class 语句执行的时候,Python 到底在背后调用了什么?

class Foo(Base): ... 的时候,大多数人不会去想这行代码背后发生了什么——它其实是一次真实的函数调用,调用的对象叫"元类"(metaclass)。这一篇本机真实拦截了这次调用,证明 class 语句就是语法糖;顺着真实源码扒出了这次调用完整的四步流程;最后用一个真实场景说明,为什么大多数时候你压根不需要碰这个东西。

"Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don't (the people who actually need them know with certainty that they need them, and don't need an explanation about why)." —— Tim Peters(《Python 之禅》作者),comp.lang.python 邮件列表
4 步
真实源码里 class 语句展开后的完整调用流程
0 个
本机真实对比:用 __init_subclass__ 实现同样的自动注册,不需要写一个元类

问题:类本身,也是某个东西的实例

Python 里"万物皆对象"这句话是认真的——包括类本身。

实例

5 是 int 的实例

type(5)int——这个大家都熟。

类也是实例

int 是 type 的实例

type(int)type——类这个东西本身,也是某个"类的类"的实例,这个"类的类"就叫元类。

递归的尽头

type 是它自己的实例

type(type) 还是 type——递归到这里打住,type 是自己的元类,是这条链的不动点。

真实实测
print(type(5))
print(type(int))
print(type(type))
print(int.__class__ is type)
<class 'int'> <class 'type'> <class 'type'> True

默认情况下,你写的每一个类,它的元类都是 type——元类这个概念一直都在起作用,只是平时感觉不到,因为大家都在用同一个默认的 typemetaclass= 做的事情,就是把这个默认值换成你自己写的东西。

真实实验:class 语句就是一次函数调用

自定义一个元类,重写 __new__/__init__,亲眼看看 class 语句执行时它们什么时候被调用——以及创建实例的时候它们还会不会再被调用一次。

真实实测
class LoudMeta(type):
    def __new__(mcs, name, bases, namespace, **kwargs):
        print(f'LoudMeta.__new__ called: name={name!r}')
        return super().__new__(mcs, name, bases, namespace, **kwargs)
    def __init__(cls, name, bases, namespace, **kwargs):
        print(f'LoudMeta.__init__ called on {cls}')
        super().__init__(name, bases, namespace, **kwargs)

print('=== defining class Foo ===')
class Foo(metaclass=LoudMeta):
    x = 1

print()
print('=== instantiating Foo() ===')
f = Foo()
=== defining class Foo === LoudMeta.__new__ called: name='Foo' LoudMeta.__init__ called on <class '__main__.Foo'> === instantiating Foo() ===

LoudMeta.__new__/__init__class Foo 这行代码执行的瞬间就被调用了,不是等到有人写 Foo() 创建实例才触发——因为 class 语句本身就是在"创建 Foo 这个对象",而元类就是负责创建它的那个"构造函数"。后面 f = Foo() 完全没有再打印任何东西,因为那次调用的是 Foo 自己的 __new__/__init__(实例创建),和元类不是一回事。

真实源码:class 语句展开后的完整四步

class 语句编译后,真的就是在调用一个叫 __build_class__ 的内置函数——反汇编能直接看到。

真实实测
import dis
code = compile('class Foo(Base):\n    x = 1\n', '<test>', 'exec')
dis.dis(code)
LOAD_BUILD_CLASS PUSH_NULL LOAD_CONST (<code object Foo>) MAKE_FUNCTION LOAD_CONST ('Foo') LOAD_NAME (Base) CALL 3 STORE_NAME (Foo)

LOAD_BUILD_CLASS + CALL,传的三个参数是:类体编译成的函数、类名、基类——这就是 __build_class__(func, name, *bases, **kwds) 的真实调用。这个内置函数的 C 实现,做的事情正好是四步:

Python/bltinmodule.c · cpython @ v3.14.7, L144 /* 第一步:确定元类——显式 metaclass= 给了就用它;没给的话, * 没有基类就用 type,有基类就用第一个基类的类型 */ if (meta == NULL) { if (PyTuple_GET_SIZE(bases) == 0) { meta = (PyObject *) (&PyType_Type); } else { PyObject *base0 = PyTuple_GET_ITEM(bases, 0); meta = (PyObject *)Py_TYPE(base0); } } /* 再用 _PyType_CalculateMetaclass 在所有基类的元类之间找"最派生"的那个 */
Python/bltinmodule.c · cpython @ v3.14.7, L183 /* 第二步:ns = meta.__prepare__(name, bases) —— 拿到一个"容器", * 类体里的赋值语句接下来都会往这个容器里塞东西 */ ns = PyObject_VectorcallDict(prep, pargs, 2, mkw); ... /* 第三步:把类体代码在 ns 这个命名空间里跑一遍 —— * class 里每一条 "x = 1" 本质就是 ns['x'] = 1 */ cell = _PyEval_Vector(tstate, (PyFunctionObject *)func, ns, NULL, 0, NULL); ... /* 第四步:cls = meta(name, bases, ns) —— 真正把类对象造出来 */ cls = PyObject_VectorcallDict(meta, margs, 3, mkw);

这四步解释了元类到底能插手什么:第一步决定"用谁来造"(可以强行指定一个自定义元类);第二步决定"类体执行时用什么容器收集属性"(可以自定义,下面马上会演示这一步能做到什么程度);第四步是"最终拍板"这个类到底长什么样——注册、校验、批量修改属性,都发生在这一步。

真实源码 + 实验:元类冲突是怎么判定的

继承多个基类,如果它们的元类互不相关,Python 没法决定该用哪个,会直接报错。

Objects/typeobject.c · cpython @ v3.14.7, L3921 /* Determine the most derived metatype. */ PyTypeObject * _PyType_CalculateMetaclass(PyTypeObject *metatype, PyObject *bases) { winner = metatype; for (each base in bases) { tmptype = Py_TYPE(base); if (PyType_IsSubtype(winner, tmptype)) continue; if (PyType_IsSubtype(tmptype, winner)) { winner = tmptype; continue; } /* 谁都不是谁的子类型,判不出胜负 */ PyErr_SetString(PyExc_TypeError, "metaclass conflict: ..."); return NULL; } return winner; }
真实实测
class MetaA(type): pass
class MetaB(type): pass
class Base1(metaclass=MetaA): pass
class Base2(metaclass=MetaB): pass
class Combined(Base1, Base2): pass
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

报错信息和源码里那句字面量一字不差。规则很直白:多个基类的元类之间,必须有一个是"最派生"的(是其他所有元类的子类),否则 Python 没法决定听谁的。

真实实验:__prepare__ 能做到教科书例子做不到的事

前面第二步提到 __prepare__ 能自定义"容器"——如果这个容器在写入时做校验,就能在类体刚执行到一半、类对象还不存在的时候拦截错误。

真实实测
class NoDupeDict(dict):
    def __setitem__(self, key, value):
        if key in self and not key.startswith('__'):
            raise TypeError(f'duplicate attribute {key!r}')
        super().__setitem__(key, value)

class StrictMeta(type):
    @classmethod
    def __prepare__(mcs, name, bases, **kw):
        return NoDupeDict()

class Bad(metaclass=StrictMeta):
    x = 1
    y = 2
    x = 3   # 手滑写重了
TypeError: duplicate attribute 'x'

普通的类定义里,x = 1 后面再来一次 x = 3,Python 什么都不会说,后一个悄悄覆盖前一个——这是很多真实 bug 的来源(复制粘贴改名字漏了一处)。靠自定义 __prepare__ 返回的命名空间拦截重复赋值,是普通 Python 代码做不到的事,因为普通代码没有机会介入"类体执行期间"这个阶段。

真实对比:Tim Peters 那句话到底在说什么

"如果你不确定要不要用元类,那你不需要"——用一个最经典的元类使用场景(自动注册所有子类)实测对比。

真实实测
# 写法一:元类
class PluginMeta(type):
    registry = {}
    def __new__(mcs, name, bases, ns):
        cls = super().__new__(mcs, name, bases, ns)
        if bases: PluginMeta.registry[name] = cls
        return cls
class PluginBase(metaclass=PluginMeta): pass
class CSVPlugin(PluginBase): pass
print('元类写法:', PluginMeta.registry)

# 写法二:__init_subclass__(3.6+,专门为了减少对元类的需求而加的)
class PluginBase2:
    registry = {}
    def __init_subclass__(cls, **kw):
        super().__init_subclass__(**kw)
        PluginBase2.registry[cls.__name__] = cls
class XMLPlugin(PluginBase2): pass
print('__init_subclass__ 写法:', PluginBase2.registry)
print('PluginBase2 自己的元类还是:', type(PluginBase2) is type)
元类写法: {'CSVPlugin': <class '__main__.CSVPlugin'>} __init_subclass__ 写法: {'XMLPlugin': <class '__main__.XMLPlugin'>} PluginBase2 自己的元类还是: True

两种写法效果完全一样,但 __init_subclass__ 版本从头到尾没有定义任何元类——PluginBase2 的元类仍然是普通的 type__init_subclass__(PEP 487,3.6 加入)和 __set_name__ 就是为了把"子类创建时想干点什么"这类最常见的需求,从元类里搬出来,变成普通类就能用的钩子——这也是 Tim Peters 那句话成立的真正原因:凡是能用这两个钩子解决的,就不必真的去写元类。真正必须用元类的场景,是要动第二步(__prepare__ 自定义命名空间)或者第一步(强行统一多个不相关基类的元类)这类 __init_subclass__ 够不着的地方。

演示:一次 class 语句,四步流程走一遍

复现前面"自动注册插件"的场景,包含一次真实会被拦截的重复赋值。

__build_class__ 四步流程演示
当前命名空间(类体正在往这里写)
PluginMeta.registry(第四步构造成功后写入)

数据来自独立验证过的 Python 模拟,复现了真实四步流程(解析元类 → __prepare__ → 执行类体 → 构造类);第三个类故意重复赋值同一个属性,模拟里同样会在"执行类体"阶段被拦截,和真实实验的报错时机一致,过程经过独立回放交叉校验。

参考与说明

  • 本文全部真实实验(class 语句触发元类调用、dis 反汇编看 __build_class__、元类冲突报错、__prepare__ 拦截重复赋值、__init_subclass__ 对比)均在本机真实运行的 Python 3.14.7 上完成,数据未做删改。
  • 源码引用(Python/bltinmodule.c__build_class__ 四步实现、Objects/typeobject.c_PyType_CalculateMetaclass)取自 python/cpython 仓库 v3.14.7 标签,与本机安装的 Python 3.14.7 完全一致。文中源码引用为节选和转述,省略了错误处理、引用计数管理等与本文主题无关的细节,不影响所述逻辑本身的准确性。
  • Tim Peters 的引言转述自其在 comp.lang.python 邮件列表上的公开发言,是被广泛引用的一段话(常见于 Python 社区和多本书籍),原文英文照录,未做翻译改写以保留语气。
  • 演示动画的四步流程模型(解析元类、准备命名空间、执行类体、构造类)由 Python 独立实现并交叉验证过——重复赋值的拦截时机通过独立回放算法核对过,不是照抄真实 C 源码,而是复现了它对外可观察的行为规则。
  • 没有涉及:abc.ABCMeta 等标准库里实际在用的元类案例、元类和描述符协议(__get__/__set__)结合使用的场景、__set_name__ 钩子的具体机制。下一篇会看到一个真实框架(Django 的 ModelBase)如何重度依赖元类把类属性变成数据库字段。
☕ 如果这篇文章帮到你,可以请作者喝杯咖啡 · 爱发电