CPython C API 类型对象深度解析:PyTypeObject 结构、类型检查、Watcher 机制与堆类型创建
本文围绕 CPython 官方文档 Doc/c-api/type.rst 展开,系统讲解 CPython C API 中"类型对象(Type Objects)"的完整能力面:PyTypeObject 结构体、PyType_Type 元类型、类型检查函数族(PyType_Check / PyType_IsSubtype)、类型缓存与失效机制(PyType_ClearCache / PyType_Modified / 类型 Watcher)、以及创建堆类型(heap type)的 PyType_FromSlots 与各类 slot 语义。读完本文,你将能够阅读并扩展 CPython 的类型系统实现,在 C 扩展中正确检查、修改和创建 Python 类型,并理解版本标签(version tag)、attribute cache 与解释器内 tp_watched 位图的底层工作机制。
1. PyTypeObject:描述内置类型的 C 结构体
文档首先给出核心数据结构:
PyTypeObject:The C structure of the objects used to describe built-in types. (用于描述内置类型的对象所用的 C 结构体。)
以及元类型对象:
PyTypeObject PyType_Type:This is the type object for type objects; it is the same object astypein the Python layer. (类型对象的"类型的类型",与 Python 层的type是同一个对象。)
在源码中,PyTypeObject 是前向声明 + 完整定义的组合:
- 前向声明位于 Include/pytypedefs.h:
typedef struct _typeobject PyTypeObject; - 完整结构体定义位于 Include/cpython/object.h 的
struct _typeobject。
从源码结构看,struct _typeobject 的字段布局大致分为几段(摘自 Include/cpython/object.h):
| 字段段 | 代表字段 | 作用 |
|---|---|---|
| 头部 | PyObject_VAR_HEAD、tp_name、tp_basicsize、tp_itemsize |
对象头与分配信息("<module>.<name>" 打印格式名) |
| 标准操作槽 | tp_dealloc、tp_vectorcall_offset、tp_repr、tp_hash、tp_call、tp_getattro、tp_setattro、tp_as_number、tp_as_sequence、tp_as_mapping、tp_as_buffer、tp_as_async |
各类协议的方法指针,部分以 PyNumberMethods 等聚合结构体指针形式存在 |
| 标志与文档 | tp_flags(unsigned long)、tp_doc |
类型特性位与文档字符串 |
| GC 相关 | tp_traverse、tp_clear、tp_is_gc、tp_finalize |
循环 GC 协议钩子 |
| 反射/描述符 | tp_methods、tp_members、tp_getset、tp_descr_get、tp_descr_set、tp_dictoffset、tp_weaklistoffset |
方法表、成员表、getset 表与实例字典/弱引用偏移 |
| 继承信息 | tp_base、tp_bases、tp_mro、tp_subclasses、tp_dict、tp_weaklist |
父类、MRO、子类集合与命名空间 |
| 构造/析构 | tp_init、tp_alloc、tp_new、tp_free、tp_del |
实例生命周期钩子 |
| 版本标签 | tp_version_tag(unsigned int) |
类型属性缓存的版本标记,见 第 4 节 |
| VM 内部字段 | tp_watched、tp_versions_used、_tp_iteritem、_tp_cache |
注释明确标注 "Below here all fields are internal to the VM",包括类型 watcher 位图与特化缓存 |
几点值得注意的实现细节(均可在 Include/cpython/object.h 确认):
- 结构体上方注释提示:修改该结构时须同步更新 Doc/includes/typestruct.h,文档中的结构体展示与该文件保持一致;
- 紧随其后定义了堆类型的真实布局
PyHeapTypeObject(struct _heaptypeobject):在ht_type(即PyTypeObject)之后依次内嵌as_async、as_number、as_mapping、as_sequence、as_buffer等协议结构体,再跟ht_name、ht_slots、ht_qualname、ht_cached_keys、ht_module、_ht_tpname、ht_token(即Py_tp_token槽的存储处)、_spec_cache(字节码特化器使用的专用缓存)等字段。这解释了为何"堆类型"比静态内置类型占用的内存更多,也是第 6 节 slot 机制能够动态挂接协议方法的结构基础; struct _specialization_cache的注释明确说明其成员"由特化机制设置,并由PyType_Modified失效",这把第 4 节的缓存失效与字节码特化直接关联起来。
2. 类型判定函数族:PyType_Check 与 PyType_IsSubtype
文档给出了四个用于判断"是不是类型 / 是否子类型"的函数:
2.1 PyType_Check 与 PyType_CheckExact
int PyType_Check(PyObject *o)
int PyType_CheckExact(PyObject *o)
PyType_Check:若 o 是类型对象则返回非零——包括标准类型对象派生类型的实例;其他情况返回 0。该函数总是成功(不会失败、不设异常)。PyType_CheckExact:若 o 是类型对象、但不是标准类型对象的子类型则返回非零;其他情况返回 0。同样总是成功。
两者区别正是"宽松判定"与"精确判定":自定义元类的实例会通过 PyType_Check,但不会通过 PyType_CheckExact。
2.2 PyType_IsSubtype
int PyType_IsSubtype(PyTypeObject *a, PyTypeObject *b)
Return true if a is a subtype of b.
文档特别强调:该函数只检查真正的子类型关系,即不会调用 b 上的 type.__subclasscheck__。如果需要与 issubclass 完全一致的语义(包括虚拟子类的元类钩子),应使用 PyObject_IsSubclass。
从源码看,PyType_IsSubtype 位于 Objects/typeobject.c,其内部路径依赖 MRO 比较——这也是为什么自定义 MRO 的类型(见 type_mro_modified,Objects/typeobject.c)会导致属性缓存版本机制被禁用:一旦 MRO 不再可缓存,版本标签机制就会对该类型失效(tp_versions_used 被置为 _Py_ATTR_CACHE_UNUSED,见 Include/cpython/object.h 的注释)。
2.3 相关的特性检查函数
文档还列出了几个按位标志快速判定函数:
unsigned long PyType_GetFlags(PyTypeObject* type) /* 3.2 新增,3.4 起返回 unsigned long */
int PyType_HasFeature(PyTypeObject *o, int feature)
int PyType_FastSubclass(PyTypeObject *type, int flag)
int PyType_IS_GC(PyTypeObject *o)
int PyType_SUPPORTS_WEAKREFS(PyTypeObject *type)
PyType_GetFlags:返回 type 的tp_flags成员。文档说明它主要为Py_LIMITED_API(受限 API)设计:各个标志位(flag bits)在不同 Python 版本间保证稳定,但直接访问tp_flags字段本身并不属于受限 API。PyType_HasFeature:若类型对象 o 设置了特性 feature 返回非零;特性用单比特标志表示。PyType_FastSubclass:若类型对象 type 设置了子类型标志 flag(Py_TPFLAGS_*_SUBCLASS系列宏)返回非零。文档指出它被许多常见类型的_Check函数使用;对于没有子类型标志的类型,应使用更慢的替代方案PyObject_TypeCheck。PyType_IS_GC:测试Py_TPFLAGS_HAVE_GC标志,判断类型是否包含循环检测器(cycle detector)支持。PyType_SUPPORTS_WEAKREFS:若 type 的实例支持创建弱引用则返回 true,否则 false;该函数总是成功,且 type 不得为NULL。文档另指引参考weakref文档与模块。
3. 类型命名空间:PyType_GetDict 与名称查询函数
3.1 PyType_GetDict(3.12 新增)
PyObject* PyType_GetDict(PyTypeObject* type)
返回类型对象的内部命名空间(即 Python 层 type.__dict__ 暴露的是只读代理映射背后的那个字典)。文档说明:
- 它是对直接访问
tp_dict字段的替代;返回的字典必须按只读对待; - 它面向特定的嵌入(embedding)与语言绑定场景——确有必要直接拿到 dict、而经由代理对象或
PyObject_GetAttr的间接访问不够用时; - 扩展模块在建立自己的类型时,仍应使用
tp_dict(直接或经由 slot)。
3.2 名称查询函数(3.11 / 3.13 新增)
PyObject* PyType_GetName(PyTypeObject *type) /* 3.11,等价于取 __name__ */
PyObject* PyType_GetQualName(PyTypeObject *type) /* 3.11,等价于取 __qualname__ */
PyObject* PyType_GetFullyQualifiedName(PyTypeObject *type) /* 3.13 */
PyObject* PyType_GetModuleName(PyTypeObject *type) /* 3.13,等价于取 __module__ */
PyType_GetFullyQualifiedName 的语义在文档中给出精确定义:等价于 f"{type.__module__}.{type.__qualname__}";但如果 type.__module__ 不是字符串、或者等于 "builtins",则等价于 type.__qualname__。
从源码看,这些函数统一走 type_name() 路径(Objects/typeobject.c 中 PyType_GetName 直接调用 type_name((PyObject *)type, NULL)),实现上通过 _PyType_Lookup 在类型字典中查找对应属性,与 Python 层取属性的结果保持一致。
4. 类型缓存、版本标签与修改通知:PyType_ClearCache、PyType_Modified
4.1 PyType_ClearCache(3.16 起为 no-op)
unsigned int PyType_ClearCache()
文档说明:清除内部查找缓存,返回当前的版本标签(version tag)。并带有版本变更注记:
versionchanged 3.16: This function is now a no-op as the type cache is now implemented per-type. It still returns the current version tag.
从源码可以印证这一点:Objects/typeobject.c 中 PyType_ClearCache 的实现只是取出解释器状态并返回 NEXT_VERSION_TAG(interp) - 1,不再遍历或清空任何全局缓存。也就是说,属性查找缓存已从"全解释器共享"演进为每个类型一份(对应 PyTypeObject 中的 tp_version_tag 与 tp_versions_used 字段),因此全局清空操作退化为"只报告当前版本标签"的空操作。
4.2 PyType_Modified:修改类型后必须调用
void PyType_Modified(PyTypeObject *type)
文档要求:任何手动修改类型属性或基类之后,必须调用该函数,以失效该类型及其所有子类型的内部查找缓存。
从源码看,Objects/typeobject.c 中的实现非常精炼:
void
PyType_Modified(PyTypeObject *type)
{
// Quick check without the lock held
if (FT_ATOMIC_LOAD_UINT_RELAXED(type->tp_version_tag) == 0) {
return;
}
BEGIN_TYPE_LOCK();
_PyType_Modified_Unlocked(type);
END_TYPE_LOCK();
}
要点:
- 先做一次无锁快速检查:如果
tp_version_tag本来就是 0(缓存从未启用),直接返回; - 否则在类型锁(type lock)内执行
_PyType_Modified_Unlocked,该路径除了更新版本标签体系,还会把堆类型的特化缓存_spec_cache.getitem置回NULL(Objects/typeobject.c 的注释写明:该字段在类型被修改时"必须"被失效,呼应 Include/cpython/object.h 中struct _specialization_cache的契约); - 版本标签的切换细节见
set_version_unlocked(Objects/typeobject.c):把旧版本从type_version_cache哈希槽中移除、递增tp_versions_used、以原子存储写入新的tp_version_tag,并注册进新槽位;tp_versions_used达到上限(MAX_VERSIONS_PER_CLASS)或遇到自定义 MRO 时,该类型的属性缓存会被永久禁用(_Py_ATTR_CACHE_UNUSED,约等于 30000,见 Include/cpython/object.h)。
4.3 配套辅助函数
int PyUnstable_Type_AssignVersionTag(PyTypeObject *type) /* 3.12 新增,Unstable API */
尝试为给定类型分配一个版本标签:返回 1 表示类型已有有效版本标签或成功分配了新标签;返回 0 表示无法分配新标签。
void* PyType_GetSlot(PyTypeObject *type, int slot) /* 3.4 新增;3.10 起可接受所有类型 */
返回给定 slot 中存储的函数指针;返回 NULL 表示 slot 为 NULL 或调用参数非法。调用者通常会把返回的指针强制转换为相应的函数类型。slot 的取值见 PyType_Slot.slot 的文档(即第 6.2 节的 slot ID)。
5. 类型 Watcher 机制(3.12 新增,3.15 增强)
这是本文档中"最重"的一组 API:扩展模块可以注册回调,当某类型发生 PyType_Modified 报告的变化时收到通知。对字节码特化、JIT、依赖属性布局的 C 扩展尤其重要。
5.1 四个函数与回调类型
int PyType_AddWatcher(PyType_WatchCallback callback) /* 3.12 */
int PyType_ClearWatcher(int watcher_id) /* 3.12 */
int PyType_Watch(int watcher_id, PyObject *type) /* 3.12;3.15 起被监视的堆类型析构时也会触发回调 */
int PyType_Unwatch(int watcher_id, PyObject *type) /* 3.12;type 不得为 NULL */
PyType_AddWatcher:注册 callback 为类型监视器,返回一个非负整数 ID,之后的PyType_Watch/PyType_Unwatch/PyType_ClearWatcher都必须携带该 ID;出错(例如 watcher ID 用尽)时返回-1并设置异常。文档明确警告:在 free-threaded(无 GIL)构建中,PyType_AddWatcher不是线程安全的,必须在启动阶段(派生第一个线程之前)调用。PyType_ClearWatcher:清除由 watcher_id 标识的 watcher(该 ID 必须先前由PyType_AddWatcher返回),成功返回0,出错返回-1(例如 watcher_id 从未注册过)。文档强调:扩展绝不应该用一个不是自己PyType_AddWatcher调用返回值的 ID 来调用它。PyType_Watch:把 type 标记为被监视。此后每当PyType_Modified报告 type 发生变化,PyType_AddWatcher授予 watcher_id 的回调就会被调用。文档给出一个实现细节注记:如果一系列连续修改之间没有对 type 调用_PyType_Lookup,回调可能对"同一串连续修改"只被调用一次——"这是实现细节,可能变化"。3.15 变更:被监视的堆类型被释放(dealloc)时回调也会被调用。PyType_Unwatch:撤销一次PyType_Watch。type 不得为NULL;成功返回0,失败返回-1并设置异常。同样禁止使用非本扩展持有的 watcher_id。
回调函数类型:
int (*PyType_WatchCallback)(PyObject *type)
文档对该回调划定了严格的行为边界:
- 回调不得修改 type,也不得导致对 type 或其 MRO 中任何类型调用
PyType_Modified——违反将可能导致无限递归; - 回调可能在类型析构期间被调用(3.15 起):此时类型对象被"临时复活"(引用计数至少为 1),其所有属性仍然有效;但回调不得对类型建立新的强引用,否则会把对象重新"复活"并阻止其析构。
5.2 源码实现印证
Objects/typeobject.c 中的实现与文档逐条对应:
int
PyType_AddWatcher(PyType_WatchCallback callback)
{
PyInterpreterState *interp = _PyInterpreterState_GET();
// start at 1, 0 is reserved for cpython optimizer
for (int i = 1; i < TYPE_MAX_WATCHERS; i++) {
if (!interp->type_watchers[i]) {
interp->type_watchers[i] = callback;
return i;
}
}
PyErr_SetString(PyExc_RuntimeError, "no more type watcher IDs available");
return -1;
}
可见 watcher 表是每解释器一份的 type_watchers 数组,ID 从 1 开始分配(0 号保留给 CPython 优化器/特化器使用),ID 用尽时报 RuntimeError。validate_watcher_id(Objects/typeobject.c)则解释了文档中"不要使用别人的 ID"的原因:非法或未注册 ID 会抛出 ValueError。
PyType_Watch 的核心仅三行(Objects/typeobject.c):
// ensure we will get a callback on the next modification
BEGIN_TYPE_LOCK();
assign_version_tag(interp, type);
type->tp_watched |= (1 << watcher_id);
END_TYPE_LOCK();
- 先保证该类型已分配版本标签(否则
PyType_Modified的"快速检查"会直接跳过回调); - 再在
tp_watched位图中置位——这正是PyTypeObject结构体中注释为 "bitset of which type-watchers care about this type" 的unsigned char tp_watched字段(Include/cpython/object.h)。
6. 堆类型创建(Creating Heap-Allocated Types)
6.1 PyType_FromSlots:新的首选入口
PyObject *PyType_FromSlots(const PySlot *slots)
从 PySlot 数组创建并返回一个堆类型(heap type)。文档说明:
- 该函数会在新类型上调用
PyType_Ready; - 它并不完全等价于调用
type()或使用class语句。若提供了用户自定义的基类型或元类,文档建议改为**直接调用type(或元类)**而不是PyType_From*系列函数。具体差异是:不会对新类调用object.__new__(且它必须是type.__new__)、不会对新类调用object.__init__、不会对任何基类调用object.__init_subclass__、不会对新描述符调用object.__set_name__; - slot 通常定义为全局静态常量数组;但当 slot 值在编译期无法静态确定时(例如
Py_tp_bases、Py_tp_metaclass、Py_tp_module需要活的 Python 对象),文档推荐把这些 slot 放在栈上,并用Py_slot_subslots引用静态 slot 数组。文档给出的示例代码:
static const PySlot my_slots[] = {
PySlot_STATIC_DATA(Py_tp_name, "MyClass"),
PySlot_FUNC(Py_tp_repr, my_repr_func),
...
PySlot_END
};
PyObject *make_my_class(PyObject *module) {
PySlot all_slots[] = {
PySlot_STATIC_DATA(Py_slot_subslots, my_slots),
PySlot_DATA(Py_tp_module, module),
PySlot_END
};
return PyType_FromSlots(all_slots);
}
从源码看,PyType_FromSlots 位于 Objects/typeobject.c,它只是公共实现 type_from_slots_or_spec 的一个薄封装;下文的 PyType_FromMetaclass、PyType_FromModuleAndSpec、PyType_FromSpecWithBases、PyType_FromSpec 同样都汇聚到同一个 type_from_slots_or_spec(Objects/typeobject.c),即新旧两套 API 共享同一条创建路径。
6.2 Type slot IDs(类型槽 ID)
文档用一整节说明 slot 的命名与特殊语义:
命名规则:大多数 slot ID 与 PyTypeObject、PyNumberMethods、PySequenceMethods、PyMappingMethods、PyAsyncMethods 等结构体的字段名相同,外加 Py_ 前缀。例如:
Py_tp_dealloc用于设置PyTypeObject.tp_deallocPy_nb_add用于设置PyNumberMethods.nb_addPy_sq_length用于设置PySequenceMethods.sq_length
需要额外注意的 slot:Py_tp_name、Py_tp_basicsize 与 Py_tp_extra_basicsize、Py_tp_itemsize、Py_tp_flags。
不对应 PyTypeObject 结构字段的额外 slot:Py_tp_token、Py_tp_metaclass、Py_tp_module。
不能通过 PyType_Slot 设置的"offset"字段:
tp_weaklistoffset(尽可能改用Py_TPFLAGS_MANAGED_WEAKREF)tp_dictoffset(尽可能改用Py_TPFLAGS_MANAGED_DICT)tp_vectorcall_offset(在PyMemberDef中使用"__vectorcalloffset__")
若无法改用 MANAGED 标志(例如 vectorcall,或需要兼容 3.12 之前的 Python),则在 Py_tp_members 中指定 offset。
创建堆类型时完全不可设置的内部字段:tp_dict、tp_mro、tp_cache、tp_subclasses、tp_weaklist。
Py_tp_base 与 Py_tp_bases 的关系:Py_tp_base 等价于 Py_tp_bases,两者都可设置为一个类型或一个类型元组;若同时指定,以 Py_tp_bases 的取值为准。
slot 值不得为 NULL 的例外:Py_tp_doc;Py_tp_token(文档建议为了清晰优先使用 Py_TP_USE_SPEC 而非 NULL)。
版本注记(文档原文逐条保留):
- 3.9 起:
PyBufferProcs中的 slot 可在"unlimited API"(不受限 API)中设置; - 3.11 起:
bf_getbuffer与bf_releasebuffer在受限 API 中可用; - 3.14 起:
tp_vectorcall字段可用Py_tp_vectorcall设置; - 3.15 起:
Py_tp_bases可设置为单个类型对象,等价于Py_tp_base槽(此前要求类型元组)。
各特殊 slot 的独立说明:
-
Py_tp_name(3.15 新增 slot 形式):类型名的 slot ID,用于设置tp_name。创建类型时必须提供它(或PyType_Spec.name);不能用于PyType_Spec.slots中(应改用PyType_Spec.name)。文档附实现细节:CPython 按顺序处理 slot,推荐把Py_tp_name放在 slots 数组开头,这样后续 slot 处理失败时的错误信息可以带上类型名。 -
Py_tp_basicsize(3.15):实例字节大小的 slot,用于设置tp_basicsize;值必须为正。不能用于PyType_Spec.slots(改用PyType_Spec.basicsize);不能与PyType_GetSlot配合使用——需要时直接读tp_basicsize,但注意类型大小通常被视为实现细节。 -
Py_tp_extra_basicsize(3.15):类型数据大小——即实例相对超类额外需要的空间。该值与超类大小一起用于设置tp_basicsize,Python 会按需插入填充以满足tp_basicsize的对齐要求;用PyObject_GetTypeData获取以此方式保留的、属于子类的那块内存的指针。值必须为正;若实例无需额外空间(即大小应继承),应省略该 slot 而不是置零。Py_tp_basicsize与Py_tp_extra_basicsize同时指定是错误。不能用于PyType_Spec.slots(改用负的PyType_Spec.basicsize),也不能与PyType_GetSlot配合。 -
Py_tp_itemsize(3.15):变长类型单个元素的字节大小,用于设置tp_itemsize,必须为正。若省略该 slot,tp_itemsize从基类继承。文档专门警告:扩展任意的变长类是危险的,因为某些类型对变长内存使用固定偏移,可能与子类的定长内存重叠。为防误用,只有以下情况允许继承itemsize:基类不是变长类(其tp_itemsize为 0);请求的PyType_Spec.basicsize为正(暗示基类内存布局已知);请求的basicsize为零(暗示子类不直接访问实例内存);或设置了Py_TPFLAGS_ITEMS_AT_END标志。 -
Py_tp_flags(3.15):类型标志,用于设置tp_flags。注意Py_TPFLAGS_HEAPTYPE标志不会由你设置,PyType_FromSpecWithBases会自动置上。读取请改用PyType_GetFlags(不可与PyType_GetSlot配合)。 -
Py_tp_bases:类型基类的 slot,可设置为类型对象元组(类似 Python 类定义的"位置参数"),3.15 起也可设置为单个类型对象(效果与单元素元组相同)。 -
Py_tp_base:等价于Py_tp_bases;若两者都指定,Py_tp_bases优先、Py_tp_base被忽略。3.15 起,若不必兼容旧版 Python,建议优先使用Py_tp_bases(Py_tp_base被标记为 soft-deprecated)。 -
Py_tp_metaclass(3.15):构造结果类型对象所用元类的 slot。省略时,元类从基类推导。支持的限制:元类若覆盖了tp_new则不被支持(tp_new为NULL除外)。不能用于PyType_Spec.slots(应使用PyType_FromMetaclass);查询时改用对类型对象调用Py_TYPE。 -
Py_tp_module(3.15):记录新类定义所在模块。值必须是模块对象;该模块与新类型关联,之后可用PyType_GetModule取回。关联的模块不会被子类继承,必须为每个类单独指定。 -
Py_tp_token(3.14):记录类的静态内存布局 ID。若类由静态分配的PyType_Spec定义,可用特殊值Py_TP_USE_SPEC(展开为NULL,仅可用于以PyType_Spec定义的类)把 token 设为该 spec 本身:static PyType_Slot foo_slots[] = { {Py_tp_token, Py_TP_USE_SPEC},也可设为任意指针,但必须保证:该指针活得比类久(类存在期间不会被复用于他物),且属于类所在的扩展模块(避免与其他扩展冲突)。用
PyType_GetBaseByToken检查某类的超类是否具有给定 token——即检查内存布局是否兼容;只取某个类自身(不考虑超类)的 token,则用PyType_GetSlot传Py_tp_token。 -
Py_tp_slots(3.15):作用类似Py_slot_subslots,但指定的是PyType_Slot结构数组(而不是PySlot数组)。
6.3 PyType_Freeze(3.14 新增)
文档说明:没有 Py_TPFLAGS_IMMUTABLETYPE 标志创建的堆类型是可修改的(例如可以像 Python 代码定义的类那样在其上设置属性)。有时这些修改是完整初始化所必需的,但初始化完成后你可能希望阻止用户继续改它:
int PyType_Freeze(PyTypeObject *type)
把类型变为不可变:设置 Py_TPFLAGS_IMMUTABLETYPE 标志。要求所有基类都不可变;成功返回 0,出错设置异常并返回 -1。类型在被冻结之前不得被使用——例如,在类型变为不可变之前不得创建其实例。
从源码印证(Objects/typeobject.c):PyType_Freeze 先取 __mro__(源码注释引用 gh-121654:检查 __mro__ 而非 __bases__),用 check_immutable_bases 验证基类不可变链,然后在类型锁内 types_stop_world()(暂停其他线程对类型的修改)→ 置 Py_TPFLAGS_IMMUTABLETYPE 标志 → types_start_world() → 调用 _PyType_Modified_Unlocked 使缓存一致。这与 3.2 节讲的版本标签机制形成闭环:冻结本身也是一次"修改",需要走同一失效路径。
7. 模块关联 API:PyType_GetModule 家族
这组函数解决的是"在 C 扩展的 slot 方法里拿回所属模块及其状态"这一经典难题:
PyObject* PyType_GetModule(PyTypeObject *type) /* 3.9 */
void* PyType_GetModuleState(PyTypeObject *type) /* 3.9 */
PyObject* PyType_GetModuleByToken(PyTypeObject *type, const void *mod_token) /* 3.15 */
PyObject* PyType_GetModuleByDef(PyTypeObject *type, struct PyModuleDef *def) /* 3.11 */
int PyType_GetBaseByToken(PyTypeObject *type, void *tp_token, PyTypeObject **result) /* 3.14 */
PyType_GetModule:返回类型创建时(经由PyType_FromModuleAndSpec)关联的模块对象。返回的是借用引用(borrowed reference),只要你持有对 type 的引用它就有效,不要用Py_DECREF释放。若无关联模块,设置TypeError并返回NULL。文档特别提示:在方法内用PyType_GetModule(Py_TYPE(self))可能得不到预期结果——Py_TYPE(self)可能是子类,而子类未必定义在超类所在模块;应使用PyCMethod调用约定拿到"定义该方法的类",或在无法使用PyCMethod时用PyType_GetModuleByToken。PyType_GetModuleState:PyModule_GetState(PyType_GetModule(type))的快捷方式。无关联模块时设置TypeError返回NULL;有模块但其 state 为NULL时不设异常直接返回NULL(这两种语义不同,需要区分处理)。PyType_GetModuleByToken(3.15):沿 MRO 找到第一个其模块具有给定模块 token(module token)的超类并返回该模块;找不到则抛TypeError返回NULL。设计用途就是与PyModule_GetState()搭配,在无法传递"定义类"的 slot 方法(如tp_init、nb_add)等位置中获取模块状态。PyType_GetModuleByDef(3.11):找到第一个其模块由给定PyModuleDef *def创建、或模块 token 等于 def 的超类并返回该模块。文档说明:由PyModuleDef创建的模块其 token 恒为该PyModuleDef的地址,因此该函数等价于PyType_GetModuleByToken,只是两点差异——返回借用引用;参数类型不是void*(仅是 C 层面的表面差异)。返回的引用同样是从 type 借用的,不得释放。PyType_GetBaseByToken(3.14):在 type 的方法解析顺序(MRO)中找到第一个Py_tp_tokentoken 等于 tp_token 的超类。返回值三态语义:找到 → 设置 *result 为指向它的新强引用,返回1;未找到 → 设置 *result 为NULL,返回0;出错 → 设置 *result 为NULL、返回-1并设异常。result 允许为NULL(此时不写入),仅需要返回值时可省略;tp_token 不得为NULL。
8. 分配与实例化的通用处理器
PyObject* PyType_GenericAlloc(PyTypeObject *type, Py_ssize_t nitems)
PyObject* PyType_GenericNew(PyTypeObject *type, PyObject *args, PyObject *kwds)
PyType_GenericAlloc:类型对象tp_alloc槽的通用处理器。使用 Python 默认内存分配机制为新实例分配内存,把内存清零,然后如同调用PyObject_Init/PyObject_InitVar一样初始化内存。文档明确:不要直接调用它来分配对象内存——应调用类型的tp_alloc槽。对支持 GC 的类型(设置了Py_TPFLAGS_HAVE_GC),其行为类似PyObject_GC_New/PyObject_GC_NewVar(区别在于初始化前内存保证为零),应在tp_free中配对PyObject_GC_Del;否则类似PyObject_New/PyObject_NewVar(同样保证清零),在tp_free中配对PyObject_Free。PyType_GenericNew:tp_new槽的通用处理器,使用该类型的tp_alloc槽创建新实例并返回。
9. PyType_Ready:类型对象的最终化
int PyType_Ready(PyTypeObject *type)
文档说明:最终化(finalize)一个类型对象——所有类型对象都应调用它以完成初始化;该函数负责把基类的继承槽填充进来。成功返回 0,出错返回 -1 并设置异常。
文档附有一条重要 note(GC 协议自动继承规则):
如果某个基类实现了 GC 协议,而所给类型的 flags 中不包含
Py_TPFLAGS_HAVE_GC,则 GC 协议会从父类自动实现。反之,如果正在创建的类型的 flags 包含Py_TPFLAGS_HAVE_GC,那么它必须自己实现 GC 协议,至少要实现tp_traverse句柄。
第 6.1 节已提到 PyType_FromSlots 等创建函数内部会替你调用 PyType_Ready;对于静态定义 PyTypeObject 字面量的传统扩展,则需要在模块初始化时手动(或经由类型最终化机制)确保它被调用。
10. 软弃用的 Spec 系列 API 与 PyType_Spec 结构
文档专门设"Soft-deprecated API"一节,说明:以下函数属于软弃用(soft deprecated)——继续可用,但新特性将作为 PyType_FromSlots 的 slot 提供,而不再是新的 PyType_From* 函数的参数:
PyObject* PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module, PyType_Spec *spec, PyObject *bases) /* 3.12;下一版本起软弃用 */
PyObject* PyType_FromModuleAndSpec(PyObject *module, PyType_Spec *spec, PyObject *bases) /* 3.9 */
PyObject* PyType_FromSpecWithBases(PyType_Spec *spec, PyObject *bases) /* 3.3 */
PyObject* PyType_FromSpec(PyType_Spec *spec)
四个函数是同一调用链上的兼容包装,等价关系在文档中逐一定义(源码亦印证:见 Objects/typeobject.c,全部委托给 type_from_slots_or_spec):
PyType_FromModuleAndSpec≡PyType_FromMetaclass(NULL, module, spec, bases)PyType_FromSpecWithBases≡PyType_FromMetaclass(NULL, NULL, spec, bases)PyType_FromSpec≡PyType_FromMetaclass(NULL, NULL, spec, NULL)
参数到 slot 的映射(PyType_FromMetaclass 文档):非 NULL 的 metaclass 对应 Py_tp_metaclass 槽;非 NULL 的 bases 对应 Py_tp_bases 槽,并优先于 slot 中的同名项;非 NULL 的 module 对应 Py_tp_module 槽。所有这些函数都会对新类型调用 PyType_Ready,且同样不完全等价于 type() / class 语句(差异清单见 PyType_FromSlots 一节)。
各函数的版本沿革(保留文档原文要点):
PyType_FromMetaclass:3.12 新增;标注"下一版本起软弃用——新代码请使用PyType_FromSlots"。PyType_FromModuleAndSpec:3.9 新增;3.10 起接受单个类作为 bases、接受NULL的tp_docslot;3.12 起会查找并使用与所提供基类匹配的元类(此前只返回type实例),且元类的tp_new被忽略,可能导致初始化不完整——"创建元类覆盖tp_new的类已被弃用";3.14 起不再允许元类覆盖tp_new;下一版本起软弃用。PyType_FromSpecWithBases:3.3 新增;3.12、3.14 的变更同上;下一版本起软弃用。PyType_FromSpec:3.12 起查找并使用与 Py_tp_base[s] slot 提供的基类对应的元类,元类覆盖tp_new的情况在 3.14 起不再允许;下一版本起软弃用。
PyType_Spec 结构体
文档定义:
PyType_Spec:Structure defining a type's behavior, used for soft-deprecated functions likePyType_FromMetaclass。(定义类型行为的结构体,用于PyType_FromMetaclass这类软弃用函数。)
其中若干成员如今可以改用 PyType_FromSlots 的 slot 表达,且它带有一个更简单结构的 slot 条目数组:
| 成员 | 对应关系 | 备注 |
|---|---|---|
const char* name |
Py_tp_name |
类型名 |
int basicsize |
正数 → Py_tp_basicsize;负数 → Py_tp_extra_basicsize(取绝对值) |
3.12 起允许负值(此前不允许) |
int itemsize |
Py_tp_itemsize |
变长类型元素大小 |
unsigned int flags |
Py_tp_flags |
类型标志 |
PyType_Slot *slots |
见下 | PyType_Slot 结构数组,以特殊值 {0, NULL} 终止;每个 slot ID 至多指定一次 |
内嵌的 PyType_Slot 结构体:"Structure defining optional functionality of a type",成员为 int slot(对应 PySlot.sl_id)与 void *pfunc(对应 PySlot.sl_ptr)。文档说明 PyType_Slot 数组可用 Py_tp_slots 包含进 PySlot 数组,反之用 Py_slot_subslots 互相嵌套。每个 PyType_Slot 条目 tpslot 被解释为等价的 PySlot:
(PySlot){
.sl_id=tpslot.slot,
.sl_flags=PySlot_INTPTR | sub_static,
.sl_ptr=tpslot.func
}
其中 sub_static 为:该 slot 本身要求静态(如 Py_tp_methods)时取 PySlot_STATIC,或(若存在)"父级" Py_tp_slots slot 上带有该标志。
11. 实战小结:C 扩展中处理类型对象的决策路径
综合本文档内容与 Objects/typeobject.c、Include/cpython/object.h 的源码证据,可以归纳出如下工程化决策路径:
- 判断对象是否为类型:优先
PyType_Check(含元类实例);要精确匹配时用PyType_CheckExact;子类型关系用PyType_IsSubtype(纯结构判定)或PyObject_IsSubclass(含虚拟子类语义)。 - 读取类型的标志 / 特性:受限 API 用
PyType_GetFlags;GC 支持用PyType_IS_GC;弱引用支持用PyType_SUPPORTS_WEAKREFS;常见类型的_Check快速路径背后是PyType_FastSubclass+Py_TPFLAGS_*_SUBCLASS标志。 - 修改类型属性或基类之后:必须调用
PyType_Modified,它会(在类型锁内)重置版本标签、清空堆类型的字节码特化缓存,并触发tp_watched位图对应的 watcher 回调。3.16 起不再需要依赖全局的PyType_ClearCache(它已是仅返回版本标签的 no-op)。 - 需要感知类型变更的扩展(如维护属性布局映射、特化/缓存组件):在启动阶段(free-threaded 构建下务必早于多线程)
PyType_AddWatcher,然后对目标类型PyType_Watch;注意回调内禁止修改类型、禁止建立新的强引用。 - 创建堆类型:新代码优先
PyType_FromSlots+PySlot数组(静态槽入静态数组、运行时槽上栈并用Py_slot_subslots组合);初始化完成后用PyType_Freeze冻结。需要兼容 3.9~3.14 环境时再用PyType_FromModuleAndSpec/PyType_FromSpecWithBases等软弃用 API,并牢记元类覆盖tp_new已不被允许(3.14 起)。 - 在 slot 方法中取模块状态:能用
PyCMethod约定就用它;否则用PyType_GetModuleByToken(3.15)/PyType_GetModuleByDef(3.11)沿 MRO 找模块,再PyModule_GetState。
12. 延伸阅读与源码索引
- 本文核心文档:Doc/c-api/type.rst(Type Objects 一节,
typeobjects锚点) PyTypeObject完整字段:Include/cpython/object.h(struct _typeobject与PyHeapTypeObject)- 文档展示用的类型结构镜像:Doc/includes/typestruct.h
- 类型系统 C 实现:Objects/typeobject.c,重点函数:
PyType_ClearCache(L994-L999)、PyType_AddWatcher/PyType_Watch/PyType_Unwatch(L1017-L1095)、set_version_unlocked(L1097-L1127)、PyType_Modified(L1207-L1218)、PyType_FromSlots/PyType_FromMetaclass等(L5764-L5794)、PyType_Freeze(L12441-L12467) - 相关文档锚点:受限 API(
limited-c-api)、堆类型(heap-types)、slot 通用说明(capi-slots)、模块 token(ext-module-token)、PyMemberDef偏移量说明(pymemberdef-offsets)、weakref模块与weakrefobjects
适用前提:本文所有版本注记(3.9 / 3.11 / 3.12 / 3.14 / 3.15 / 3.16)以当前仓库 Doc/c-api/type.rst 的 versionadded / versionchanged / soft-deprecated 标记为准;其中 PyType_FromSlots、Py_tp_* 系列新 slot 与 PyType_Freeze 等属于当前主干(next 版本)能力,移植到旧版本 Python 时请改走 PyType_Spec 系列 API。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00