Chapter 13: Class Construction
Python 的 class 语句会生成一个类对象。读完本章后,你应能追踪一个类从语法块变成可调用类型对象的路径,判断类体里的名字进入哪个 namespace,区分 metaclass、__prepare__、type.__new__、__init_subclass__、__slots__ 和 __class__ cell 分别影响哪一个阶段。
类创建的核心链路是:解释器先处理类头部,确定 bases、metaclass 和类 namespace;再把 class body 当作代码块执行,把赋值结果写入这个 namespace;随后调用 metaclass 创建类对象;最后执行 descriptor 命名、子类初始化钩子、装饰器应用和外层名字绑定。Python 3.14 语言参考把 class definition 归入 code block,并说明 class creation 包含 MRO entry 解析、metaclass 选择、namespace 准备、class body 执行和 class object 创建几个阶段;本章使用这个模型组织正文,而 CPython 细节以 CPython 3.6+ 和 Python 3.8+ 的 __classcell__ 行为为边界说明。Python 3.14 execution model Python 3.14 data model: customizing class creation
本章贯穿下面这个短例子。它同时包含 class body 赋值、descriptor、__prepare__、metaclass、__init_subclass__、__slots__ 和零参数 super(),足够覆盖类创建链路中最容易混淆的状态变化。
class NamedField:
def __set_name__(self, owner, name):
self.owner = owner
self.name = name
class RegistryBase:
registry = []
def __init_subclass__(cls, category, **kwargs):
super().__init_subclass__(**kwargs)
cls.category = category
RegistryBase.registry.append(cls)
class RecordingMeta(type):
@classmethod
def __prepare__(mcls, name, bases, **kwargs):
return {"declared_names": []}
def __new__(mcls, name, bases, namespace, **kwargs):
namespace["declared_names"].extend(namespace.keys())
return super().__new__(mcls, name, bases, namespace)
class User(RegistryBase, metaclass=RecordingMeta, category="account"):
__slots__ = ("id",)
field = NamedField()
def show_owner(self):
return __class__.__name__
这段代码执行完成后,User 是一个类对象,type(User) 是 RecordingMeta,User.category 来自父类钩子,User.field.name 来自 descriptor 命名回调,User.__slots__ 改变实例布局,show_owner 中的 __class__ 通过编译器创建的 cell 指向最终类对象。下面各节把这个结果拆回创建路径。
13.1 Class body execution and namespace creation
Class body 是一个代码块。它在类创建过程中执行一次,把赋值、函数定义、嵌套类定义、descriptor 实例、__slots__ 声明等结果写入类 namespace。这个 namespace 先服务于类对象创建,随后被复制成类对象的属性字典。
在贯穿例子中,User 的类体执行时,field = NamedField() 会先创建一个 NamedField 实例,再把名字 field 绑定到类 namespace 中。def show_owner(self): ... 会创建一个 function object,再把名字 show_owner 绑定到同一个 namespace 中。__slots__ = ("id",) 也只是一次类体赋值,但 type.__new__ 后续会把它解释成实例布局约束。
类体执行可以近似理解成下面的路径:
namespace = RecordingMeta.__prepare__("User", (RegistryBase,), category="account")
# 执行 class body,把 __slots__、field、show_owner 等名字写入 namespace
User = RecordingMeta("User", (RegistryBase,), namespace, category="account")
这个近似模型只用于解释对象关系。真实解释器还会处理 decorators、__class__ cell、MRO entries、错误路径和版本相关实现细节。关键判断是:class body 的赋值目标先进入临时 namespace,类名 User 在类对象创建完成后才绑定到外层 namespace。
类体 namespace 有一个常见边界:类体中定义的名字可在同一个 class block 后续语句中读取,但方法体执行时走方法自己的函数 frame。方法体里的普通名字查找不会自动读取类 namespace。下面的代码能说明这个边界:
class Sample:
prefix = "S"
label = prefix + "-label"
def method(self):
return prefix
label = prefix + "-label" 在类体执行阶段读取类 namespace 中的 prefix。method 调用时,prefix 是函数体里的普通名字,查找路径进入 local、enclosing、global、builtins,而类 namespace 不作为方法普通名字查找的一层。稳定写法是通过 self.prefix、type(self).prefix、Sample.prefix 或闭包中的 __class__ 读取类对象上的属性。
这条规则能解释许多调试现象。类体里的 print(prefix) 可以工作,方法里的 return prefix 会触发 NameError。原因在执行阶段和 namespace 角色:类体执行时正在填充 class namespace;方法调用时正在执行 function code object,类对象只通过属性访问协议参与查找。
当自定义 metaclass 提供 __prepare__ 时,class body 写入的 namespace 可以是普通 dict 之外的 mapping。Python 3.7+ 的普通 dict 已保持插入顺序,但 __prepare__ 仍能定制写入监控、重复定义检查、声明顺序记录和 DSL 风格收集。贯穿例子的 RecordingMeta.__prepare__ 返回带有 declared_names 的字典,让 metaclass 后续能读取类体声明过的键。
一个可复用检查顺序是:先确认某个名字出现在哪个代码块;再确认它是类体赋值、函数局部变量、外层闭包变量还是模块全局变量;最后确认读取发生在类体执行阶段、方法调用阶段还是类对象创建之后。这样可以把“类里写了这个名字,为什么方法里读不到”还原成执行 frame 和 namespace 的问题。
13.2 Metaclass selection and prepare
Metaclass 是创建类对象的 callable。默认情况下,普通类由 type 创建;当类头部给出 metaclass=...,或基类本身由自定义 metaclass 创建时,解释器需要先选出真正负责创建新类的 metaclass。
在 class User(RegistryBase, metaclass=RecordingMeta, category="account"): 这行中,类头部提供了三类输入:类名 User、bases 元组 (RegistryBase,)、关键字参数 metaclass=RecordingMeta 与 category="account"。metaclass 是给类创建流程消耗的提示,category 会继续传给 __prepare__、metaclass 调用路径和 __init_subclass__ 相关路径。
Metaclass 选择的基本判断顺序如下:
“最派生 metaclass”可以理解为候选 metaclass 中能同时作为其它候选子类的那个类型。这个规则使多继承下的类创建保持一致:新类的 metaclass 需要兼容所有基类已有的类型创建规则。官方 data model 对这个选择过程给出明确顺序,并说明候选不兼容时 class definition 会以 TypeError 失败。Python 3.14 data model: determining the appropriate metaclass
__prepare__ 在 metaclass 确定后执行。它的输入是类名、bases 和额外关键字参数;输出是 class body 将要写入的 namespace。贯穿例子中,解释器会近似调用:
namespace = RecordingMeta.__prepare__(
"User",
(RegistryBase,),
category="account",
)
随后 class body 的赋值都落入这个 namespace。__prepare__ 的返回对象要能支持类体写入所需的 mapping 行为。最终进入 type.__new__ 时,CPython 会把 namespace 复制成新的有序 mapping,再通过只读 proxy 暴露为类对象的 __dict__。因此,__prepare__ 返回对象的临时身份通常不会成为最终 User.__dict__ 的身份。
下面的 metaclass 用 __prepare__ 检查重复定义。这个例子展示 __prepare__ 的工程用途:它控制的是 class body 写入容器,而 class body 的每次赋值都通过这个容器完成。
class DuplicateCheckingDict(dict):
def __setitem__(self, key, value):
if key in self:
raise RuntimeError(f"duplicate class name: {key}")
super().__setitem__(key, value)
class StrictMeta(type):
@classmethod
def __prepare__(mcls, name, bases, **kwargs):
return DuplicateCheckingDict()
class Model(metaclass=StrictMeta):
field = 1
# field = 2 # would raise RuntimeError during class body execution
这个错误会发生在类对象创建之前,因为 class body 写入 field 的动作被 namespace 捕获。若错误发生在 StrictMeta.__new__ 中,class body 已经执行完成;若错误发生在父类 __init_subclass__ 中,类对象已经由 type.__new__ 创建。定位这类问题时,先判断异常来自 namespace 写入、metaclass 构造、descriptor 回调还是父类钩子。
__prepare__ 的边界也很明确。它适合决定类体写入如何被收集,适合收集声明顺序、阻止重复名字、构造 ORM 或验证 DSL 声明。它不适合在这里依赖最终类对象,因为最终类对象此时尚未存在。需要访问最终类对象的逻辑应放入 metaclass __new__、metaclass __init__、descriptor __set_name__ 或 __init_subclass__。
13.3 type.new, type.init, and class object creation
Class body 执行完成后,namespace 中已经有一组名字到对象的绑定。下一步是调用 metaclass,创建真正的类对象。对大多数类而言,这条路径最终会进入 type.__new__ 和 type.__init__。
贯穿例子中的关键调用可以近似写成:
User = RecordingMeta(
"User",
(RegistryBase,),
namespace,
category="account",
)
RecordingMeta 是 type 的子类。调用一个 metaclass 时,仍然遵守对象调用协议:先走 metaclass 的 __call__,再进入它的 __new__ 和 __init__。通常自定义 metaclass 会覆盖 __new__ 或 __init__。__new__ 负责创建类对象,__init__ 负责在类对象已经产生后做初始化。多数场景下,__new__ 更适合修改 namespace、决定 bases、注册 descriptor 前置状态;__init__ 更适合读取已经创建好的类对象并做登记。
type.__new__ 接收 name、bases、namespace 和关键字参数,输出一个类对象。这个类对象拥有 __name__、__module__、__qualname__、__mro__、__dict__、slot 布局、descriptor 表和一系列类型级结构。Python 层面看到的 User.__dict__ 是只读 mapping proxy,它保护类字典容器自身;通过 User.new_attr = value 仍会走类型对象的属性设置路径并更新类属性。
这一步还会触发 descriptor 命名回调。官方 data model 说明,type.__new__ 创建类时会扫描 class namespace,找到定义了 __set_name__ 的对象,并以 (owner, name) 调用它们。Python 3.14 data model: object.set_name 贯穿例子中,field = NamedField() 被写入 namespace 后,type.__new__ 创建 User 时会调用:
User.field.__set_name__(User, "field")
因此,User.field.owner is User,User.field.name == "field"。这个回调的输入来自最终 owner 和 class namespace 中的键名,适合让 descriptor 获得自己被赋给哪个类属性。若在类创建后再执行 User.other = NamedField(),自动 __set_name__ 已经错过;需要手动调用 User.other.__set_name__(User, "other"),或把命名逻辑放入其它注册路径。
type.__init__ 的位置在类对象已经创建之后。对普通类创建,它一般不会像 type.__new__ 那样重建布局。自定义 metaclass 覆盖 __init__ 时,应把它理解成“类对象创建后初始化”,例如把类注册到某个索引中:
class IndexingMeta(type):
classes = []
def __init__(cls, name, bases, namespace, **kwargs):
super().__init__(name, bases, namespace)
IndexingMeta.classes.append(cls)
这里的 cls 已经是类对象。它可以读取 cls.__mro__、cls.__dict__、descriptor 状态和父类关系。若要改变 __slots__、bases 解析或 namespace 中 descriptor 对象本身,__new__ 更直接;若只做注册、校验和补充属性,__init__ 更清晰。
Class decorators 在类对象创建后执行。它们接收刚创建出来的类对象,返回将要绑定到外层名字的对象。换句话说,type.__new__ 产物和最终名字绑定值可能不同:
def replace_class(cls):
return {"original": cls}
@replace_class
class Wrapped:
pass
执行后,外层名字 Wrapped 绑定到 decorator 返回的字典,原始类对象只保存在这个字典中。这个边界对调试很有价值:metaclass、descriptor 和 __init_subclass__ 看到的是原始类对象;外层代码通过类名看到的是 decorators 链处理后的结果。
这一节的检查顺序是:先看 metaclass 调用是否到达 type.__new__;再看 namespace 是否被修改或复制;再看 descriptor __set_name__ 是否在类创建时自动执行;最后看 decorators 是否改变最终绑定值。这样能把“类属性为什么已经存在”“descriptor 为什么知道自己的名字”“类名为什么不是一个 type 对象”分到不同阶段解释。
13.4 init_subclass, slots, and class cell
类对象创建后,Python 还会执行几个影响后续行为的收尾步骤。本节关注三个高频边界:父类的 __init_subclass__ 钩子、__slots__ 布局声明、以及支撑零参数 super() 和 __class__ 引用的 class cell。
__init_subclass__ 是父类接收“有新子类出现”这个事件的钩子。它定义在父类上,在子类创建完成后由类创建流程调用。贯穿例子中,User 继承 RegistryBase,类头部传入 category="account",因此 RegistryBase.__init_subclass__ 会收到这个关键字参数:
class RegistryBase:
registry = []
def __init_subclass__(cls, category, **kwargs):
super().__init_subclass__(**kwargs)
cls.category = category
RegistryBase.registry.append(cls)
这里的 cls 是新创建的子类 User。这让父类可以统一登记子类、检查约束、填充默认类属性。__init_subclass__ 适合表达“所有子类都需要遵守的创建后规则”。它比 metaclass 更局部,因为它挂在普通父类上;它也比 class decorator 更靠近继承关系,因为它由父类参与新子类创建。
metaclass 关键字在类创建流程中被消耗,不会继续传给 __init_subclass__。贯穿例子中,category 可以传给父类钩子,metaclass=RecordingMeta 用于确定类创建者。若父类钩子没有接收某个类头部关键字,也没有通过 **kwargs 转交给 super().__init_subclass__,创建子类时就可能在钩子调用阶段失败。
__slots__ 是类体中的特殊名字。它告诉 type.__new__ 为实例准备固定槽位,典型效果是实例可以存储声明的 slot 字段,并且默认不创建实例 __dict__。在贯穿例子中:
class User(RegistryBase, metaclass=RecordingMeta, category="account"):
__slots__ = ("id",)
User() 的实例可以有 id 这个 slot 字段。若类层级没有提供实例 __dict__,给实例设置未声明名字会失败。这个行为属于实例布局层面,和类对象本身的属性字典是两个层级。User.category、User.field、User.declared_names 是类属性;user.id 是实例 slot 字段。
__slots__ 的工程判断顺序是:先看当前类和基类是否声明了 slot;再看是否显式包含 __dict__;再看 descriptor、property 和普通实例属性是否使用相同名字;最后确认错误发生在实例属性设置阶段还是类对象创建阶段。它影响实例对象的内存布局和属性存储位置,也会影响动态追加属性的能力。
Class cell 解决的是类对象在方法闭包中的引用问题。零参数 super() 需要知道“当前定义所在的类”,方法体中显式写 __class__ 也需要引用最终类对象。类对象在 class body 执行期间尚未创建,所以编译器会为需要 __class__ 的方法准备一个 cell,类创建完成后再把最终类对象填入这个 cell。
贯穿例子的 show_owner 触发了这个路径:
class User(RegistryBase, metaclass=RecordingMeta, category="account"):
def show_owner(self):
return __class__.__name__
__class__ 在这里不是普通全局变量,也不是类 namespace 中的普通属性读取。它是编译器为方法建立的词法引用。CPython 3.6+ 会把这个 cell 以 __classcell__ 条目放入 class namespace,交给 metaclass 创建类对象;Python 3.8+ 要求自定义 metaclass 在调用 type.__new__ 时正确传递这个条目,否则会触发 RuntimeError。官方 data model 对这个 __classcell__ 传播要求有 CPython 实现边界说明。Python 3.14 data model: creating the class object
这给自定义 metaclass 一个明确约束:修改 namespace 时要保留 __classcell__。下面的写法展示了安全转交的形态:
class SafeMeta(type):
def __new__(mcls, name, bases, namespace, **kwargs):
namespace = dict(namespace)
namespace["created_by"] = mcls.__name__
return super().__new__(mcls, name, bases, namespace)
这个例子复制了整个 namespace,因此 __classcell__ 会随其它键一起传给 type.__new__。风险写法通常出现在“手动挑选 namespace 键”时:如果只复制业务字段,把 __classcell__ 丢掉,零参数 super() 或方法中的 __class__ 就会在类创建阶段暴露错误。
这一节的核心边界可以压缩成三句话:__init_subclass__ 面向“新子类已创建”事件;__slots__ 面向“实例字段存储布局”;class cell 面向“方法闭包如何引用最终类对象”。三者都出现在类创建链路后段,但它们影响的对象层级不同。
13.5 Dynamic class creation path
动态建类指运行时用函数调用构造类对象。它与 class 语句共享同一套核心规则:解析 bases、选择 metaclass、准备 namespace、填充 namespace、调用 metaclass、执行 type.__new__ 后续步骤。稳定理解动态建类的关键,是把“类语法糖”还原成显式数据流。
最直接的动态建类是调用 type(name, bases, namespace):
DynamicUser = type(
"DynamicUser",
(object,),
{
"kind": "dynamic",
"field": NamedField(),
},
)
这会创建一个名为 DynamicUser 的类对象。因为最终进入 type.__new__,field 的 __set_name__ 仍会自动执行。这个例子说明 descriptor 命名回调属于类对象创建流程,和是否写 class 关键字无直接绑定。
更完整的动态建类应使用 types.new_class。它会根据类名、bases 和关键字参数选择合适的 metaclass,并把一个新 namespace 交给 exec_body 回调填充。Python 标准库文档说明,types.new_class 使用合适的 metaclass 动态创建类,exec_body 接收刚创建的 class namespace 并直接更新它;types.prepare_class 则负责计算合适的 metaclass 并创建 class namespace。Python 3.14 types: dynamic type creation
下面的代码等价地构造一个带 metaclass 和父类钩子的类:
from types import new_class
def fill_user_namespace(namespace):
namespace["__slots__"] = ("id",)
namespace["field"] = NamedField()
def show_owner(self):
return type(self).__name__
namespace["show_owner"] = show_owner
GeneratedUser = new_class(
"GeneratedUser",
(RegistryBase,),
{"metaclass": RecordingMeta, "category": "account"},
fill_user_namespace,
)
new_class 的好处是它复用 class statement 的 metaclass 选择和 namespace 准备规则。若直接调用 type(...),你需要自己确认 metaclass 是否兼容 bases、是否需要 __prepare__、关键字参数应该传向哪里。动态 ORM、序列化模型、插件系统和测试替身经常需要动态建类,此时 types.new_class 比手写 metaclass(name, bases, namespace) 更接近语言规则。
动态建类时的检查顺序可以固定为五步。第一步,检查 bases 是否需要 __mro_entries__ 解析,尤其是泛型别名、typing 相关对象或自定义基类占位对象。第二步,检查 metaclass 是否和所有 bases 兼容。第三步,检查 namespace 是否由 __prepare__ 创建,以及填充逻辑是否写入了 __module__、__qualname__、descriptor、__slots__ 等必要字段。第四步,确认 metaclass 最终是否调用 type.__new__,因为 descriptor __set_name__、slot 布局和 class cell 依赖这条路径。第五步,确认 decorators 或外层注册逻辑是否改变最终绑定值。
对于 CPython 源码阅读,可以把动态建类和 class statement 连接到同一条内部路径。语言层面的 class statement 最终会通过内置的 __build_class__ 完成类创建调度;CPython 源码中可从 CPYTHON_SRC/Python/bltinmodule.c 中的 __build_class__ 相关实现、CPYTHON_SRC/Objects/typeobject.c 中的 type.__new__ 相关实现、以及编译阶段处理 class body 的代码生成逻辑入手。这里的源码路径只是阅读入口,当前正文的判断以 Python language reference 和标准库 types 文档规定的语义为准。
本章建立的最终判断是:类创建是一条分阶段 runtime 路径。类体先作为代码块执行并填充 namespace;metaclass 决定 namespace 如何准备和类对象如何构造;type.__new__ 把 namespace 转换成类对象并触发 descriptor、slots、class cell 等后续语义;动态建类只要遵守同一条数据流,就能得到和 class 语句一致的对象行为。
最小自检任务
阅读下面的代码,判断 Plugin.name、Plugin.registry、Plugin.label.owner、Plugin().run() 和 Plugin().extra = 1 的结果或错误阶段。回答时按类创建链路说明每一步发生在哪个阶段。
class Label:
def __set_name__(self, owner, name):
self.owner = owner
self.name = name
class Base:
registry = []
def __init_subclass__(cls, name, **kwargs):
super().__init_subclass__(**kwargs)
cls.name = name
cls.registry.append(cls)
class Plugin(Base, name="demo"):
__slots__ = ()
label = Label()
def run(self):
return __class__.name
答案要点
Plugin.name 的值是 "demo"。这个属性来自父类 Base.__init_subclass__,调用发生在 Plugin 类对象创建完成之后,外层名字 Plugin 绑定之前。
Plugin.registry 读取到的是 Base.registry 这个列表,列表中已经追加了 Plugin。cls.registry.append(cls) 先通过类属性查找找到父类上的列表,再修改同一个列表对象,因此登记结果对 Base.registry 和 Plugin.registry 都可见。
Plugin.label.owner is Plugin 为真,Plugin.label.name == "label"。label = Label() 在 class body 阶段把 descriptor 实例写入 namespace;type.__new__ 创建类对象时扫描 namespace,并调用 label.__set_name__(Plugin, "label")。
Plugin().run() 返回 "demo"。方法体中的 __class__ 通过 class cell 指向最终类对象 Plugin,再读取 Plugin.name。这个读取路径不同于在方法体里直接写普通名字 name。
Plugin().extra = 1 会在实例属性设置阶段失败。Plugin 声明了空 __slots__,当前类没有给实例提供 extra slot;在没有实例 __dict__ 的布局下,实例无法动态保存这个新名字。这个错误发生在类已经创建完成并实例化之后。
本章知识点总结
- 类体执行:Class body 是代码块,执行结果先写入类 namespace。
- 类名绑定:外层类名在类对象创建、装饰器处理之后才绑定。
- 方法查找:方法体普通名字查找不把类 namespace 当作自动查找层。
- 元类选择:新类的 metaclass 来自显式提示和各基类 metaclass 的兼容选择。
- 命名空间准备:
__prepare__决定 class body 写入的 mapping,适合收集声明顺序和检查重复名字。 - 对象创建:Metaclass 调用最终通常进入
type.__new__,把 namespace 转换成类对象。 - 命名回调:
__set_name__在类创建阶段获得 owner 和属性名,服务 descriptor 初始化。 - 父类钩子:
__init_subclass__在新子类创建后执行,适合父类集中登记和校验子类。 - Slots 布局:
__slots__影响实例属性存储位置和动态属性能力。 - Class cell:方法中的零参数
super()和__class__依赖编译器创建并由类创建流程填充的 cell。 - 动态建类:
types.new_class复用 metaclass 选择和 namespace 准备规则,适合运行时构造类。 - 检查顺序:定位类创建问题时,先看类头部,再看 namespace,接着看 metaclass,最后看 descriptor、父类钩子和装饰器。