Python元类编程:type与__new__如何拦截类的诞生
一、先捅破一层窗户纸:类也是对象
写过几年 Python 的人大多听过一句话——"一切皆对象"。但真把这句话吃透,往往要等到第一次被元类(metaclass)绊住脚。
看一段最朴素的代码:
1 | class User: |
输出里最后一行最反直觉:u 是 User 的实例,所以 type(u) 是 User;可 User 本身呢?type(User) 居然是 type。
本质一句话:普通对象是类的实例,而类本身,是"元类"的实例。 默认元类就是内置的 type。所以 class User: 这行声明,不是"定义了一个类型",而是"调用 type 构造出了一个对象,这个对象的名字叫 User"。
C++ 视角对照:在 C++ 里 class User {}; 是编译期的类型声明,类型本身不是运行期对象,你拿不到"User 这个类"的运行时实体,更不能在运行时凭空造一个类。Python 把"类"也当成一等公民的对象,这是后续一切动态魔法的总开关。
下面这张图把"实例 → 类 → 元类"三层关系画清楚(注意箭头方向是"由谁创建"):
二、type 的双重身份:既是类型,也是类工厂
type 有两个完全不像的面貌:
- 单参数
type(obj):返回 obj 的类型(上节用过)。 - 三参数
type(name, bases, namespace):动态地创建一个类。
两者是同一个对象,差别只在参数个数。三参数版本等价于一次 class 声明:
1 | # 下面三行,和上面那段 class User 完全等价 |
这正是元类的入口:当你写 class User: 时,Python 解释器真正做的是调用 type(name, bases, ns)。而"用哪个 type 来调",是可以被你换掉的——换成你自己的元类。
C++ 对照:C++ 没有运行期"类工厂"。想动态生成类型,只能靠模板在编译期实例化(template<class T> struct Wrapper {};),或者干脆代码生成(写脚本吐 .hpp)。运行期?做不到。Python 这份"类即对象、可运行时构造"的能力,是元类存在的根本前提。
三、元类就是"类的类":在 new 里拦截诞生
想自定义类的创建行为,写一个继承 type 的类,并重写它的 __new__(或 __init__):
__new__(mcs, name, bases, ns):在类对象被构造出来之前调用,返回最终的类对象。适合"改头换面"(改名字、加属性、替换方法)。__init__(cls, name, bases, ns):类对象已构造好之后调用,只能做校验,改不了它本体。
习惯上元类第一个参数叫 mcs(metaclass),以区别普通类的 cls/self。
1 | class StrictMeta(type): |
class Service(metaclass=StrictMeta) 这句,翻译过来就是:"别用默认 type 来造我,请用 StrictMeta 来造。" Python 于是执行 StrictMeta('Service', (object,), ns),把类的诞生拱手交给你的钩子。
小坑提醒:前文《Python描述符进阶》讲过描述符协议也是在"类创建时"就确定好查找裁决顺序的。元类的
__new__比描述符更早、更底层——它能在描述符甚至方法被绑定之前,就改写整个类体。
四、实战一:用元类做自动子类注册表
元类最常见的落地,是插件/子类自动登记——不用让用户手动 REGISTRY.append(SubClass),类一定义就自动入册:
1 | class PluginMeta(type): |
MySQLSink 一被定义,元类的 __new__ 就把它塞进了 registry。新增一种 sink 只需写类,无需任何注册样板——这正是 Django、SQLAlchemy 等框架里 model/plugin 机制的雏形。
五、实战二:ORM 字段校验(轻量版)
再做一个贴近真实工程的例子:用元类在类创建时,把所有标为 Field 的描述符收集成一张 schema,并强制要求主键存在:
1 | class Field: |
注意这里复用了前文《Python描述符进阶》讲的 __set_name__:描述符在类体里能反查自己挂在了哪个属性名上。元类则站在更高的位置,把整张字段表一次性收口。
C++ 视角对照表
| 需求 | Python(元类) | C++ 近似方案 | 差异 |
|---|---|---|---|
| 运行时动态创建类型 | type(name, bases, ns) 直接造类 |
不支持,类型在编译期固定 | Python 独有运行时灵活性 |
| 类创建时自动改/加成员 | 元类 __new__ 改写 namespace |
无直接对应;可用宏在预处理期注入 | C++ 只能编译期,且易踩坑 |
| 子类自动注册 | 元类 __new__ 入册 registry |
CRTP + 静态成员在构造函数注册 | C++ 靠模板递归+静态对象,样板更多 |
| 编译期约束/校验 | 元类抛 TypeError |
static_assert / std::enable_if |
C++ 是编译期硬错误,Python 是运行时 |
| "类即对象"反射 | 类本身可当参数传递、可 introspect | typeid / std::type_info 仅查类型名 |
C++ 反射极弱,无运行时改类能力 |
| 字段 schema 自动收集 | 遍历类体 namespace 即可 | 需手写注册宏或外部代码生成 | Python 内省天然支持 |
一句话总结差异:C++ 的"类元编程"发生在编译期(模板/宏),是给编译器看的;Python 的元类发生在运行时,是给解释器和程序员自己看的。 前者零运行时开销但僵硬,后者灵活但要为此付出一点运行时成本与可读性代价。
六、收尾:什么时候该用元类
元类很 powerful,但也最容易"杀鸡用牛刀"。经验法则:
- 能用classmethod/装饰器解决的,别上元类。 比如"注册表"用类装饰器
@register往往更直观。 - 元类适合横切所有子类、且必须在类创建时就介入的场景:全局注册、字段/schema 收集、统一的 API 约束、DSL 式框架(ORM、测试框架)。
- 真要写,优先重写
__new__做"构造期变换",把纯校验留给__init__;多用super(),别把type.__new__写死。
| 手段 | 介入时机 | 典型用途 | 推荐度 |
|---|---|---|---|
| 类装饰器 | 类已完整定义后 | 注册、加方法、打补丁 | ⭐⭐⭐ 首选 |
__init_subclass__ |
子类定义时(Python 3.6+) | 轻量子类钩子 | ⭐⭐⭐ 首选 |
元类 __new__ |
类体还没定型时 | 改写类体/强制 schema | ⭐⭐ 必要时 |
元类 __init__ |
类对象已生成 | 仅校验 | ⭐ 很少单独用 |
记住那张三层图:实例由类造,类由元类造。元类不是炫技工具,而是 Python 把"类本身"也变成可编程对象后,留给你的最后一道、也是最底层的一道钩子。🐾

