Python 的 class 语句到底执行了什么?拆开 __prepare__、metaclass.__new__ 和那个被讲反的 __set_name__ 顺序

写 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__ / 类装饰器解决的,就别上元类。元类是「所有其他办法都不够」时的最后一层,代价是继承链变脆------上文的冲突报错就是代价之一。

八、排错清单(实测踩过的六条)

  1. __prepare__ 返回自定义 mapping,type.__new__ 报错 → 传 dict(ns) 进去,把顺序信息另存到类属性。
  2. 以为 __set_name__ 在 __init__ 之后 → 实测在 Meta.__new__ 内部、早于 Meta.__init__;依赖它结果的代码别写在 __init__ 之前又假设已生效。
  3. metaclass conflict → 造一个同时继承所有相关元类的新元类,别硬指定。
  4. 元类 __new__ 忘记 return super().__new__ → 返回 None,后续 isinstance 检查失败,__init__ 不跑,调用方拿到 None。
  5. 在 namespace 里找函数却拿到 classmethod 对象 → 用 __func__ 取原函数;绑定发生在属性查找时。
  6. 把 __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 实测取得,未引用外部基准数据。

相关推荐
天天被压力2 小时前
【A股分钟级高频数据完全手册 #07】北交所历史分钟K线接口:京市Pro版分时批量
java·人工智能·python
天天被压力2 小时前
【A股分钟级高频数据完全手册 #08】股票日、分时数据接口对比:普通分时与Pro版差异实测
java·人工智能·python
特立独行的猫A3 小时前
AI 时代,我为什么推荐用 cj-tauri 开发桌面应用
java·后端·python
小lo想吃棒棒糖3 小时前
把机器人训练从 GPU 搬到昇腾 NPU:启元 Q1 起身动作 PPO 训练全记录
python·机器人·昇腾·启元机器人
Thanks_ks3 小时前
【第 006 讲】搭建 PyCharm 开发环境:PyCharm 的下载与安装 | 初始设置与常用配置
ide·python·pycharm·开发工具·jetbrains·开发环境·集成开发环境
青少儿编程课堂4 小时前
蓝桥杯青少组图形化省考报名倒计时·全国青少年信息素养大赛·电子学会图形化考级:校园黑白棋人机对战解析
c++·python·算法·bfs·信息学竞赛
tellmewhoisi4 小时前
nlp:知识库检索和dify知识库创建
python·自然语言处理
2601_968793145 小时前
Python Requests 轻量自动化:无需浏览器,实现真人级账号浏览模拟
开发语言·python·自动化
段一凡-华北理工大学14 小时前
工业智能体:从Codex/Claude到项目实战~系列文章05:从“AI助手“到“工业智能体“:能力跃迁与落地范式
人工智能·python·claude·codex·工业智能体·高炉炼铁智能化