写 Python 的人天天写 class,但多数教程把它讲成「一段声明」。其实 class 是一条可执行语句:执行时会先准备一个命名空间、跑一遍类体、再调用元类把这三样东西组装成一个类对象。
一句话结论:class D(...) 几乎等价于 D = metaclass(name, bases, namespace);完整顺序是 __prepare__ → 类体 → metaclass.__new__ → __set_name__ / __init_subclass__ → metaclass.__init__。其中 __set_name__ 和 __init_subclass__ 是在 __new__ 内部触发的,发生在 __init__ 之前------这一点被大量教程讲反。
下面所有输出来自本机 CPython 3.13.14 实测,脚本是 d54-probe.py。
一、先证明 class 就是一次函数调用
type 三参数可以直接造类,不需要 class 语句:
python
class Base:
pass
def hi(self):
return 'hi from ' + type(self).__name__
C = type('C', (Base,), {'x': 1, 'hi': hi, '__module__': '__main__'})
print(C.__name__, [b.__name__ for b in C.__bases__], C.x, C().hi())
print(type(C) is type, isinstance(C, type), issubclass(C, Base))
实测输出:
python
C ['Base'] 1 hi from C
True True True
三个参数分别是:类名字符串、基类元组、属性命名空间字典。返回值是一个类对象,赋给了 C。
所以这两段在语义上是一回事:
python
class C(Base):
x = 1
def hi(self): ...
# ≈
C = type('C', (Base,), {'x': 1, 'hi': ...})
区别只在于:class 语句会先由解释器准备好 namespace、执行类体,再决定用哪个元类去调用。
顺带把 type 的三个身份分清:
python
print(type(1), type(type), type(object))
arduino
<class 'int'> <class 'type'> <class 'type'>
一参数是「查类型」,三参数是「造类」,type 自己也是 type 的实例------所有类(包括元类)都是 type 或其子类造出来的。
二、完整执行链实测:那个被讲反的顺序
这段是本文的核心。我给元类、描述符、父类都埋了打点,看它们到底谁先谁后:
python
ORDER = []
class Meta(type):
@classmethod
def __prepare__(mcls, name, bases):
ORDER.append('1 __prepare__')
return {'__prepared__': True}
def __new__(mcls, name, bases, ns, **kw):
ORDER.append('3 Meta.__new__')
ns['injected'] = 42
return super().__new__(mcls, name, bases, ns)
def __init__(cls, name, bases, ns, **kw):
ORDER.append('4 Meta.__init__')
super().__init__(name, bases, ns)
class Parent:
def __init_subclass__(cls, **kw):
ORDER.append('6 __init_subclass__')
super().__init_subclass__(**kw)
class Desc:
def __set_name__(self, owner, name):
ORDER.append('5 __set_name__(' + name + ')')
self.name = name
ORDER.append('0 before class')
class D(Parent, metaclass=Meta):
ORDER.append('2 class body')
a = 1
d = Desc()
ORDER.append('7 after class')
print(' -> '.join(ORDER))
print(D.injected, '__prepared__' in D.__dict__, D.d.name, type(D).__name__)
实测输出(原样):
rust
0 before class -> 1 __prepare__ -> 2 class body -> 3 Meta.__new__ -> 5 __set_name__(d) -> 6 __init_subclass__ -> 4 Meta.__init__ -> 7 after class
42 True d Meta
注意第 5、6 步的位置。 很多资料把 __init_subclass__、__set_name__ 画在「类创建完成、元类 __init__ 之后」,实测不是:它们在 Meta.__new__ 调用 super().__new__(也就是 type.__new__)的过程中触发,因此排在 Meta.__init__ 之前。
这个顺序有实际后果:Meta.__init__ 里能看到已经被 __set_name__ 处理过的描述符(比如 D.d.name == 'd'),而 PyCharm / mypy 之类工具若按错误顺序做静态推断,就会对「描述符何时拿到名字」判断失误。
另外两个细节:
__prepare__返回的字典原样进了类 namespace:'__prepared__' in D.__dict__为 True。Meta.__new__里ns['injected'] = 42生效了,说明元类__new__是最后一道能改属性的关卡 ,之后再改就得等__init__(此时类对象已存在,改的是类属性而非 namespace 组装)。
类字典前几个键实测:
css
['__prepared__', '__module__', '__firstlineno__', 'a', 'd', '__static_attributes__', 'injected', '__doc__']
三、prepare 为什么单独存在
Python 2 里类体 namespace 是普通 dict,属性定义顺序会丢。Python 3 引入 __prepare__,让元类可以指定「类体语句往哪个 mapping 里写」:
python
type(type.__prepare__('X', ()))
arduino
<class 'dict'>
默认是 dict(3.7 起保证插入序)。但它真正的价值在自定义 mapping,比如 ORM 要按定义顺序记录字段:
python
class OrderedNS(dict):
def __init__(self):
super().__init__(); self.order = []
def __setitem__(self, k, v):
if not k.startswith('__'): self.order.append(k)
super().__setitem__(k, v)
class FieldMeta(type):
@classmethod
def __prepare__(mcls, name, bases):
return OrderedNS()
def __new__(mcls, name, bases, ns):
cls = super().__new__(mcls, name, bases, dict(ns))
cls.field_order = ns.order
return cls
class User(metaclass=FieldMeta):
id = 1
name = 2
age = 3
print(User.field_order) # ['id', 'name', 'age']
这里有个坑:type.__new__ 要的是真 dict,所以传进去前得 dict(ns) 转一下,否则报 type.__new__() argument 3 must be dict。这也是 __prepare__ 返回自定义 mapping 时最常见的报错。
四、namespace 里装的不是「最终形态」
类体里写 @classmethod、@staticmethod、@property,在 namespace 里存的是描述符对象本身,不是函数、也不是绑定后的方法:
python
class E:
@classmethod
def cm(cls): pass
@staticmethod
def sm(): pass
@property
def p(self): return 1
print(type(E.__dict__['cm']).__name__,
type(E.__dict__['sm']).__name__,
type(E.__dict__['p']).__name__)
python
classmethod staticmethod property
也就是说「绑定」发生在属性查找时(E.__dict__['cm'].__get__(None, E)),不是类创建时。这解释了为什么元类在 __new__ 里遍历 namespace 时,看到的是一串 classmethod / property 包装对象,想拿原函数得用 __func__。
五、元类冲突:一条必背的报错
python
class M1(type): pass
class M2(type): pass
class A(metaclass=M1): pass
class B(A, metaclass=M2): pass
javascript
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
规则:派生类的元类必须是所有基类元类的(非严格)子类。解法不是硬指定一个,而是造一个同时继承两者的新元类:
python
class M12(M1, M2): pass
class B(A, metaclass=M12): pass
多继承 + 多框架(比如一个基类来自 ORM、一个来自 ABC)时最容易撞这条。
六、把整条链写成伪代码
python
def class_statement(name, bases, metaclass=None, body=None):
meta = metaclass or infer_metaclass(bases) # 冲突就在这一步报
ns = meta.__prepare__(name, bases) # 1
exec(body, ns) # 2 类体写入 ns
cls = meta.__new__(meta, name, bases, ns) # 3 type.__new__ 内部触发
# -> 对每个描述符调 __set_name__(cls, k) # 5
# -> 调父类的 __init_subclass__ # 6
if isinstance(cls, meta):
meta.__init__(cls, name, bases, ns) # 4
return cls
注意 __init__ 也有 isinstance 检查(和实例创建时 type.__call__ 那道类似):若 __new__ 返回的不是该元类的实例,__init__ 会被跳过。
七、元类到底该用在哪
| 需求 | 更合适的做法 | 说明 |
|---|---|---|
| 子类定义时自动登记 | __init_subclass__ |
不需要元类,且支持多重继承 |
| 描述符需要知道自己叫啥 | __set_name__ |
Python 3.6+,替代元类扫描 |
| 给类加装饰器能解决的逻辑 | 类装饰器 | 可读性更好,不引入元类冲突 |
| 拦截实例创建过程 | 元类 __call__ |
单例、缓存、参数预处理 |
| 校验/改写类 namespace | 元类 __new__ |
框架级,如 ORM、接口校验 |
| 单纯加几个方法 | 直接写 / mixin | 别为三行代码引入元类 |
经验规则:能靠 __init_subclass__ / __set_name__ / 类装饰器解决的,就别上元类。元类是「所有其他办法都不够」时的最后一层,代价是继承链变脆------上文的冲突报错就是代价之一。
八、排错清单(实测踩过的六条)
__prepare__返回自定义 mapping,type.__new__报错 → 传dict(ns)进去,把顺序信息另存到类属性。- 以为
__set_name__在__init__之后 → 实测在Meta.__new__内部、早于Meta.__init__;依赖它结果的代码别写在__init__之前又假设已生效。 metaclass conflict→ 造一个同时继承所有相关元类的新元类,别硬指定。- 元类
__new__忘记return super().__new__→ 返回None,后续isinstance检查失败,__init__不跑,调用方拿到None。 - 在 namespace 里找函数却拿到
classmethod对象 → 用__func__取原函数;绑定发生在属性查找时。 - 把
__init_subclass__和__init__归成一类 → 前者在类被定义 时触发,后者是实例初始化;两者生命周期完全不同。
九、小结
把这句话记住就够了:class 不是声明,是一次 metaclass(name, bases, namespace) 调用;__prepare__ 给纸,类体写字,__new__ 装帧,__set_name__ 和 __init_subclass__ 在装帧过程中收尾,__init__ 最后签收。
所以下次看到「元类 __init__ 里读不到 __set_name__ 的结果」这类问题,先别怀疑人生------大概率是资料把顺序画反了,拿上面那段打点代码跑一遍就知道。
运行环境:本机 CPython 3.13.14。本文所有输出均由 d54-probe.py 实测取得,未引用外部基准数据。