一、引子:当你写下 class 时,到底发生了什么

写 Python 久了,很容易把 class 当成"定义一个模板"的声明。但 Python 里没有"声明",只有"执行"。

1
2
3
4
5
6
7
class Person:
name = "Ada"
def greet(self):
return f"Hi, I'm {self.name}"

print(type(Person)) # <class 'type'>
print(type(type(Person))) # <class 'type'>

输出:

1
2
<class 'type'>
<class 'type'>

Person 不是某种静态蓝图,它是 type 这个类当场造出来的一个实例type 就是"类的类"——也就是元类(metaclass)。当你写下 class 语句,Python 在运行期做了三件事:收集命名空间 → 调用元类 → 得到一个类对象。

换句话说:类是元类的实例,对象是类的实例。 这一层"类的类",正是元类能插手类创建的原因。

二、核心概念:元类是「创建类的可调用物」

type 本身可以当函数用,动态造类:

1
2
3
4
5
6
7
8
def greet(self):
return f"Hi, I'm {self.name}"

# type(name, bases, namespace)
Person = type("Person", (), {"name": "Ada", "greet": greet})

p = Person()
print(p.greet()) # Hi, I'm Ada

这一行 type(...) 和上面那段 class Person: 完全等价——class 语句只是语法糖,底层就是调用元类。

自定义元类,就是写一个 type 的子类,重写 __new__ / __init__,在类被创建的那一刻改点东西:

1
2
3
4
5
6
7
8
9
10
class MyMeta(type):
def __new__(mcs, name, bases, ns):
print(f"[元类] 正在创建类:{name}")
ns["__author__"] = "metaclass"
return super().__new__(mcs, name, bases, ns)

class User(metaclass=MyMeta):
pass

print(User.__author__) # metaclass

输出:

1
2
[元类] 正在创建类:User
metaclass

C++ 视角对照:C++ 没有运行期的"类对象",class 是编译期类型。最接近"类的类"的是模板元编程(TMP)type traits——比如 std::is_integral<T>::value 在编译期"询问类型的属性",std::enable_if 按类型条件启用/禁用重载。差别在于:C++ 的定制发生在编译期、零运行时开销;Python 元类发生在运行期、灵活但每次类定义都执行。一句话——C++ 的"类的类"是编译器替你生成的,Python 的"类的类"是你自己写的工厂。

1
2
3
4
Python 类创建流水线:
class 语句 → 元类.__prepare__ → 元类.__new__ → 元类.__init__ → 类对象
C++ 类"生成"流水线:
模板实例化 → 编译器推导 → 类型 traits → 编译期类型 (无运行期对象)

三、深入:用元类自动给方法加"计时"外壳

真实项目里,元类最常见的作用之一是统一给类里的所有方法注入横切行为。下面用元类给每个实例方法自动包一层执行耗时统计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import time
import functools

class TimedMeta(type):
def __new__(mcs, name, bases, ns):
new_ns = dict(ns) # 复制一份命名空间,避免改到原 dict
for key, val in ns.items():
# 只包裹用户定义的实例方法,跳过特殊方法与嵌套类
if callable(val) and not key.startswith("__"):
new_ns[key] = TimedMeta._wrap(val)
return super().__new__(mcs, name, bases, new_ns)

@staticmethod
def _wrap(func):
@functools.wraps(func) # 保留原函数的名字/文档
def wrapper(self, *args, **kwargs):
t0 = time.perf_counter()
try:
return func(self, *args, **kwargs)
finally:
cost = (time.perf_counter() - t0) * 1000
print(f"[timed] {func.__name__} 耗时 {cost:.3f} ms")
return wrapper

class Calc(metaclass=TimedMeta):
def slow(self, n):
time.sleep(0.05)
return sum(range(n))

def fast(self, n):
return n * 2

c = Calc()
c.slow(1000)
c.fast(21)

输出:

1
2
[timed] slow 耗时 50.123 ms
[timed] fast 耗时 0.002 ms

注意两个坑:第一,namespace 默认是个只读的 dict 子类,必须 dict(ns) 复制后再改;第二,包函数务必用 functools.wraps,否则方法名、__doc__ 会丢,调试时一脸懵。

四、实战:元类 vs 类装饰器,到底选谁

上面这个"给方法加计时"的需求,用类装饰器也能做:

1
2
3
4
5
6
7
8
9
10
11
12
13
import time, functools

def timed(cls):
for key, val in vars(cls).items():
if callable(val) and not key.startswith("__"):
setattr(cls, key, TimedMeta._wrap(val)) # 复用上面的 _wrap
return cls

@timed
class Calc:
def slow(self, n):
time.sleep(0.05)
return sum(range(n))

两者都能成事,区别在于作用时机

  • 类装饰器:类已经完整创建完,再整体"改一遍"。简单、可读、好调试。
  • 元类:类还在"娘胎"里(__new__ 阶段)就能改,能拿到完整的基类信息、能控制属性查找顺序、能配合 __prepare__ 自定义命名空间类型(比如强制有序字典)。

经验法则:

  • 一次性改造、简单包装 → 类装饰器足够,且更直观。
  • 需要约束子类契约(强制子类实现某些方法)、需要跨继承链统一行为、需要注入 __slots__ 或自定义描述符 → 用元类

典型的元类"名场面":Django ORM(Model 用元类把字段收集成 Meta)、SQLAlchemy、以及各大框架的"自动注册插件"机制——都是在类定义瞬间把信息汇总起来。

五、对比与总结

维度 元类 metaclass 类装饰器 C++ 模板元编程
作用时机 类创建之中(__new__ 类创建之后 编译期实例化时
可读性 较低,套路多 高,一目了然 低,报错晦涩
能力边界 可控制继承解析、命名空间、属性查找 只能改已生成的类 只能生成/约束类型
典型用途 ORM 字段收集、插件注册、接口契约 方法包装、单类改造 type traits、SFINAE、编译期计算
错误暴露 导入/定义类时报 导入/定义类时报 编译期(信息常被模板错误淹没)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
类创建流水线(一张图记住):
┌─────────────┐
│ class 语句 │
└──────┬──────┘
│ 1. 元类.__prepare__ 准备命名空间

┌─────────────┐
│ 收集方法/属性 │
└──────┬──────┘
│ 2. 元类.__new__ 真正造类(这里能改)

┌─────────────┐
│ 元类.__init__ │
└──────┬──────┘
│ 3. 得到一个类对象

class User

一句话总结:元类是 Python 运行期最深的"钩子"——它让你在类还没出生时就替它做决定。但钩子越深,越要克制:能用类装饰器解决的,就别上元类;真要上,也只用在"必须在创建期介入"的地方(契约约束、自动注册)。C++ 程序员可以把它类比成"运行期版的模板特化 + type traits",只是代价从编译期挪到了运行期。