本文目录
执行 Dog = type("Dog", (), {"breed": "unknown"}) 之后,Dog 就是一个类,可以 Dog() 造实例。这和写 class Dog: ... 殊途同归——类本身也是运行时对象,不是编译期留下的静态标签。你能在 REPL 里把类赋给变量、放进列表、当作参数传递,正是因为它和列表、函数一样,都是堆上的普通对象。
上一篇讲 __call__ 与可调用对象;这一篇把视角再抬高一层:class 语句在做什么、谁在「生产」类对象、以及 metaclass 这个词到底指哪一步。搞清时间线很重要——先有类对象,后有实例;元类参与的是前者。
若你来自 Java 或 C++,习惯把 class 当作静态声明;在 Python 里更贴切的心智模型是「运行时工厂对象」。模块 import 到 class User: 那一行时,解释器真的执行了造类逻辑,产出的 User 可以赋给别的名字、传给函数、存进字典。实例化 User() 则是第二次调用链:类对象的 __call__ 再去造实例。两层 callable,别混在一谈。
class 语句:创建类对象并绑定名字
很多人把 class Point: 理解成「声明一个类型」。在 Python 里更准确的说法是:执行一条语句,调用元类造出类对象,再把名字 Point 绑到这个对象上。模块加载时类体里的顶层代码会跑一遍,但那是类命名空间初始化的一部分,与「造类」本身是两回事。
下面两种写法,在概念上等价(细节如 __qualname__、__module__ 可能略有差别):
class Point:
x: int = 0
y: int = 0
Point2 = type("Point2", (), {"x": 0, "y": 0})
print(type(Point)) # <class 'type'>
print(type(Point2)) # <class 'type'>
print(Point is not Point2)class Point: 执行时,解释器会:
- 收集类体里的名字(方法、类属性、嵌套类、注解等),放进临时 namespace 字典;
- 确定元类(默认
type,或基类/显式metaclass=指定的类型); - 调用元类的
__new__/__init__造出类对象; - 把类对象绑到当前命名空间的
Point。
之后 Point(1, 2) 才是「用类当工厂造实例」——那是类对象的 __call__(通常落到 type.__call__),发生在类已经存在之后。改实例初始化看 __init__;改「类长什么样」才看 metaclass。
类体里的方法定义同样发生在造类阶段:函数对象先被创建,再作为类属性挂到 namespace 里。因此「类里的 def」和「模块顶层的 def」都是造函数对象,差别在于前者最终进类的 __dict__,并通过描述符协议在实例访问时绑定 self(下一篇、描述符篇会接上)。
type:默认元类
每个类对象的 type(...) 就是它的元类。普通用户定义的类,元类几乎都是内置的 type:
class User:
name: str = ""
print(type(User)) # <class 'type'>
print(isinstance(User, type)) # True
print(User.__class__ is type) # Truetype 身兼两职:作为函数,type(obj) 返回对象所属类;作为 metaclass,它在 class 语句末尾被调用来生产类对象。两个角色,同一个内置对象,初学者容易混,记住「查类型」和「造类」发生在不同阶段即可。
单参数 type(obj) 与三参数 type(name, bases, ns) 签名不同,靠参数个数区分。IDE 提示里看到 type 有多个 overload,指的就是这两种用法。
自定义 metaclass:造类阶段的钩子
元类是类的类。自定义 metaclass 继承 type,重写 __new__ 或 __init__,在类对象被创建时插入逻辑——例如统一注册、校验类属性、自动加方法、禁止某些命名。
class RegistryMeta(type):
registry: dict[str, type] = {}
def __new__(
mcls: type,
name: str,
bases: tuple[type, ...],
namespace: dict[str, object],
**kwargs: object,
) -> type:
cls = super().__new__(mcls, name, bases, namespace, **kwargs)
if name != "Base":
RegistryMeta.registry[name] = cls
return cls
class Base(metaclass=RegistryMeta):
pass
class Alpha(Base):
pass
class Beta(Base):
pass
print(RegistryMeta.registry) # {'Alpha': ..., 'Beta': ...}指定 metaclass=RegistryMeta 后,class Alpha(Base): 不再直接由 type 造类,而是交给 RegistryMeta.__new__。子类若未显式换 metaclass,通常会继承父类的元类,所以 Alpha、Beta 也会进 registry。
另一个常见钩子是在 __init__ 里做收尾——此时类对象已创建,可以改 cls.__dict__ 或打标记。框架作者用 metaclass 统一约束插件类、模型类的结构;应用代码很少手写。阅读 Django 模型、enum 或 dataclass 源码时,看到 metaclass 别慌:先找「它在 class 语句里多做了哪一步」,通常比逐行跟完整个类定义更高效。
和 init_subclass、类装饰器的取舍
Python 3.6+ 的 __init_subclass__ 能在子类定义时收到通知,不必上 metaclass。类装饰器(下一篇)则在类对象造完后包装它。经验法则:
- 只关心「谁继承了我」→ 优先
__init_subclass__; - 类已经存在,想加字段或包一层行为 → 类装饰器;
- 要在 namespace 进类对象之前删改成员、换基类解析 → 才考虑 metaclass。
__init_subclass__ 拿不到 class 体执行中的临时 namespace,只能看到已经造好的子类;若要在成员进类之前过滤或注入,仍得 metaclass。二者不是非此即彼,而是钩子粒度不同。
日常代码需要写元类吗?
绝大多数项目不需要。dataclass、abc.ABC、Django ORM 在内部用 metaclass 或等价机制,那是库作者的事。应用层更常见的是普通 class + 继承,或用装饰器做轻量注册。
读懂 metaclass 的价值在于:看到 class Foo(metaclass=BarMeta) 时,知道「改的是造类过程,不是某次 Foo() 实例化」;读 traceback 里 metaclass 的 __new__ 时不至于茫然;调试框架时知道该在哪一层下断点。
还可以用 __prepare__(在 3 6+ 的 metaclass 上)返回类 namespace 用的映射对象,例如 dict 的子类,在类体执行期间记录成员定义顺序——enum 等库用这个技巧。日常业务不必手写,知道「namespace 在进 __new__ 之前是可以被 metaclass 换掉的」即可。
type 的三参数形式:动态造类
type(name, bases, namespace) 把 class 语句拆成显式调用,在元编程、代码生成、测试替身里偶尔出现:
def greet(self: object) -> str:
return f"hi, {getattr(self, 'name', '?')}"
Dynamic = type("Dynamic", (), {"name": "bot", "greet": greet})
d = Dynamic()
print(d.greet()) # hi, botbases 必须是元组(单基类也要逗号:(Base,));namespace 是类成员映射。返回的仍是类对象,元类默认 type。动态造类不改变语义,只是把「类从哪来」暴露给你看。
元类如何被选定
写 class Child(Parent): 时,若未写 metaclass=,解释器沿 MRO 找基类的 metaclass;若基类都没有自定义元类,则用 type。显式 metaclass=M 优先级最高。多个基类元类冲突时会报错——设计继承时要留意。
class MetaA(type):
pass
class MetaB(type):
pass
class A(metaclass=MetaA):
pass
# class C(A, B): ... # 若 B 用 MetaB,可能 metaclass 冲突这就是为什么混入不同框架的基类有时会「元类不兼容」:两边各有一套 metaclass 逻辑。
常见误解
| 误解 | 实际 |
|---|---|
| metaclass 控制实例怎么初始化 | 实例化走类的 __call__ → __new__ / __init__;metaclass 管的是类对象何时、以什么内容被创建 |
| 每个类都要指定 metaclass | 默认就是 type,不写即够用 |
| metaclass 比装饰器「更高级」 | 二者作用阶段不同:metaclass 在 class 语句执行时;装饰器在类对象已存在后包装它 |
type("Name", bases, dict) 是黑魔法 | 它只是把 class 语句拆成显式调用,教学与动态造类时很有用 |
读代码时的检查清单
遇到 metaclass 相关栈帧,可以按顺序问:
- 这是造类阶段还是实例化阶段?(看栈里是
Meta.__new__还是Cls.__call__) - 类对象已经存在了吗?存在则改行为用装饰器或改类属性,别去动 metaclass。
- registry / 插件表是在类定义时填还是实例化时填?metaclass 钩子几乎总在类定义时。
元类不是「高级 Python 的黑箱」,而是 class 语句语义的自然延伸:你已经在用 type 造类,只是默认没露面。自定义 metaclass 只是把这一步换成可插拔的实现。保持「造类 vs 造实例」两条时间线清晰,读任何框架的类定义都会少一半疑惑。
面试或文档里常问「元类有什么用」——标准答案不是「每天写」,而是「读库时能认出造类阶段的钩子,并在真正需要统一约束子类结构时使用」。把 metaclass 放进工具箱最底层一格即可。
一句话:class 语句不是静态声明,而是一次运行时调用——调用元类造出类对象;type 是默认 metaclass;自定义 metaclass 是在这一步插钩子,日常少见,读库与调试时却很有用。