首页
/ CPython C API 类型对象深度解析:PyTypeObject 结构、类型检查、Watcher 机制与堆类型创建

CPython C API 类型对象深度解析:PyTypeObject 结构、类型检查、Watcher 机制与堆类型创建

2026-09-06 12:50:44作者:范垣楠Rhoda

本文围绕 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 as type in the Python layer. (类型对象的"类型的类型",与 Python 层的 type 是同一个对象。)

在源码中,PyTypeObject 是前向声明 + 完整定义的组合:

从源码结构看,struct _typeobject 的字段布局大致分为几段(摘自 Include/cpython/object.h):

字段段 代表字段 作用
头部 PyObject_VAR_HEADtp_nametp_basicsizetp_itemsize 对象头与分配信息("<module>.<name>" 打印格式名)
标准操作槽 tp_dealloctp_vectorcall_offsettp_reprtp_hashtp_calltp_getattrotp_setattrotp_as_numbertp_as_sequencetp_as_mappingtp_as_buffertp_as_async 各类协议的方法指针,部分以 PyNumberMethods 等聚合结构体指针形式存在
标志与文档 tp_flagsunsigned long)、tp_doc 类型特性位与文档字符串
GC 相关 tp_traversetp_cleartp_is_gctp_finalize 循环 GC 协议钩子
反射/描述符 tp_methodstp_memberstp_getsettp_descr_gettp_descr_settp_dictoffsettp_weaklistoffset 方法表、成员表、getset 表与实例字典/弱引用偏移
继承信息 tp_basetp_basestp_mrotp_subclassestp_dicttp_weaklist 父类、MRO、子类集合与命名空间
构造/析构 tp_inittp_alloctp_newtp_freetp_del 实例生命周期钩子
版本标签 tp_version_tagunsigned int 类型属性缓存的版本标记,见 第 4 节
VM 内部字段 tp_watchedtp_versions_used_tp_iteritem_tp_cache 注释明确标注 "Below here all fields are internal to the VM",包括类型 watcher 位图与特化缓存

几点值得注意的实现细节(均可在 Include/cpython/object.h 确认):

  1. 结构体上方注释提示:修改该结构时须同步更新 Doc/includes/typestruct.h,文档中的结构体展示与该文件保持一致;
  2. 紧随其后定义了堆类型的真实布局 PyHeapTypeObjectstruct _heaptypeobject):在 ht_type(即 PyTypeObject)之后依次内嵌 as_asyncas_numberas_mappingas_sequenceas_buffer 等协议结构体,再跟 ht_nameht_slotsht_qualnameht_cached_keysht_module_ht_tpnameht_token(即 Py_tp_token 槽的存储处)、_spec_cache(字节码特化器使用的专用缓存)等字段。这解释了为何"堆类型"比静态内置类型占用的内存更多,也是第 6 节 slot 机制能够动态挂接协议方法的结构基础;
  3. 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_modifiedObjects/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:返回 typetp_flags 成员。文档说明它主要为 Py_LIMITED_API(受限 API)设计:各个标志位(flag bits)在不同 Python 版本间保证稳定,但直接访问 tp_flags 字段本身并不属于受限 API。
  • PyType_HasFeature:若类型对象 o 设置了特性 feature 返回非零;特性用单比特标志表示。
  • PyType_FastSubclass:若类型对象 type 设置了子类型标志 flagPy_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.cPyType_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.cPyType_ClearCache 的实现只是取出解释器状态并返回 NEXT_VERSION_TAG(interp) - 1,不再遍历或清空任何全局缓存。也就是说,属性查找缓存已从"全解释器共享"演进为每个类型一份(对应 PyTypeObject 中的 tp_version_tagtp_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();
}

要点:

  1. 先做一次无锁快速检查:如果 tp_version_tag 本来就是 0(缓存从未启用),直接返回;
  2. 否则在类型锁(type lock)内执行 _PyType_Modified_Unlocked,该路径除了更新版本标签体系,还会把堆类型的特化缓存 _spec_cache.getitem 置回 NULLObjects/typeobject.c 的注释写明:该字段在类型被修改时"必须"被失效,呼应 Include/cpython/object.hstruct _specialization_cache 的契约);
  3. 版本标签的切换细节见 set_version_unlockedObjects/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_Watchtype 不得为 NULL;成功返回 0,失败返回 -1 并设置异常。同样禁止使用非本扩展持有的 watcher_id

回调函数类型:

int (*PyType_WatchCallback)(PyObject *type)

文档对该回调划定了严格的行为边界:

  1. 回调不得修改 type,也不得导致对 type 或其 MRO 中任何类型调用 PyType_Modified——违反将可能导致无限递归;
  2. 回调可能在类型析构期间被调用(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 用尽时报 RuntimeErrorvalidate_watcher_idObjects/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_basesPy_tp_metaclassPy_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_FromMetaclassPyType_FromModuleAndSpecPyType_FromSpecWithBasesPyType_FromSpec 同样都汇聚到同一个 type_from_slots_or_specObjects/typeobject.c),即新旧两套 API 共享同一条创建路径。

6.2 Type slot IDs(类型槽 ID)

文档用一整节说明 slot 的命名与特殊语义:

命名规则:大多数 slot ID 与 PyTypeObjectPyNumberMethodsPySequenceMethodsPyMappingMethodsPyAsyncMethods 等结构体的字段名相同,外加 Py_ 前缀。例如:

  • Py_tp_dealloc 用于设置 PyTypeObject.tp_dealloc
  • Py_nb_add 用于设置 PyNumberMethods.nb_add
  • Py_sq_length 用于设置 PySequenceMethods.sq_length

需要额外注意的 slotPy_tp_namePy_tp_basicsizePy_tp_extra_basicsizePy_tp_itemsizePy_tp_flags

不对应 PyTypeObject 结构字段的额外 slotPy_tp_tokenPy_tp_metaclassPy_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_dicttp_mrotp_cachetp_subclassestp_weaklist

Py_tp_basePy_tp_bases 的关系Py_tp_base 等价于 Py_tp_bases,两者都可设置为一个类型或一个类型元组;若同时指定,以 Py_tp_bases 的取值为准。

slot 值不得为 NULL 的例外Py_tp_docPy_tp_token(文档建议为了清晰优先使用 Py_TP_USE_SPEC 而非 NULL)。

版本注记(文档原文逐条保留):

  • 3.9 起:PyBufferProcs 中的 slot 可在"unlimited API"(不受限 API)中设置;
  • 3.11 起:bf_getbufferbf_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_basicsizePy_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_basesPy_tp_base 被标记为 soft-deprecated)。

  • Py_tp_metaclass(3.15):构造结果类型对象所用元类的 slot。省略时,元类从基类推导。支持的限制:元类若覆盖了 tp_new 则不被支持tp_newNULL 除外)。不能用于 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_GetSlotPy_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_GetModuleStatePyModule_GetState(PyType_GetModule(type)) 的快捷方式。无关联模块时设置 TypeError 返回 NULL;有模块但其 state 为 NULL不设异常直接返回 NULL(这两种语义不同,需要区分处理)。
  • PyType_GetModuleByToken(3.15):沿 MRO 找到第一个其模块具有给定模块 token(module token)的超类并返回该模块;找不到则抛 TypeError 返回 NULL。设计用途就是与 PyModule_GetState() 搭配,在无法传递"定义类"的 slot 方法(如 tp_initnb_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_token token 等于 tp_token 的超类。返回值三态语义:找到 → 设置 *result 为指向它的新强引用,返回 1未找到 → 设置 *resultNULL,返回 0出错 → 设置 *resultNULL、返回 -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_GenericNewtp_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_FromModuleAndSpecPyType_FromMetaclass(NULL, module, spec, bases)
  • PyType_FromSpecWithBasesPyType_FromMetaclass(NULL, NULL, spec, bases)
  • PyType_FromSpecPyType_FromMetaclass(NULL, NULL, spec, NULL)

参数到 slot 的映射(PyType_FromMetaclass 文档):非 NULLmetaclass 对应 Py_tp_metaclass 槽;非 NULLbases 对应 Py_tp_bases 槽,并优先于 slot 中的同名项;非 NULLmodule 对应 Py_tp_module 槽。所有这些函数都会对新类型调用 PyType_Ready,且同样不完全等价于 type() / class 语句(差异清单见 PyType_FromSlots 一节)。

各函数的版本沿革(保留文档原文要点):

  • PyType_FromMetaclass:3.12 新增;标注"下一版本起软弃用——新代码请使用 PyType_FromSlots"。
  • PyType_FromModuleAndSpec:3.9 新增;3.10 起接受单个类作为 bases、接受 NULLtp_doc slot;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 like PyType_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.cInclude/cpython/object.h 的源码证据,可以归纳出如下工程化决策路径:

  1. 判断对象是否为类型:优先 PyType_Check(含元类实例);要精确匹配时用 PyType_CheckExact;子类型关系用 PyType_IsSubtype(纯结构判定)或 PyObject_IsSubclass(含虚拟子类语义)。
  2. 读取类型的标志 / 特性:受限 API 用 PyType_GetFlags;GC 支持用 PyType_IS_GC;弱引用支持用 PyType_SUPPORTS_WEAKREFS;常见类型的 _Check 快速路径背后是 PyType_FastSubclass + Py_TPFLAGS_*_SUBCLASS 标志。
  3. 修改类型属性或基类之后:必须调用 PyType_Modified,它会(在类型锁内)重置版本标签、清空堆类型的字节码特化缓存,并触发 tp_watched 位图对应的 watcher 回调。3.16 起不再需要依赖全局的 PyType_ClearCache(它已是仅返回版本标签的 no-op)。
  4. 需要感知类型变更的扩展(如维护属性布局映射、特化/缓存组件):在启动阶段(free-threaded 构建下务必早于多线程)PyType_AddWatcher,然后对目标类型 PyType_Watch;注意回调内禁止修改类型、禁止建立新的强引用。
  5. 创建堆类型:新代码优先 PyType_FromSlots + PySlot 数组(静态槽入静态数组、运行时槽上栈并用 Py_slot_subslots 组合);初始化完成后用 PyType_Freeze 冻结。需要兼容 3.9~3.14 环境时再用 PyType_FromModuleAndSpec / PyType_FromSpecWithBases 等软弃用 API,并牢记元类覆盖 tp_new 已不被允许(3.14 起)。
  6. 在 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.hstruct _typeobjectPyHeapTypeObject
  • 文档展示用的类型结构镜像: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.rstversionadded / versionchanged / soft-deprecated 标记为准;其中 PyType_FromSlotsPy_tp_* 系列新 slot 与 PyType_Freeze 等属于当前主干(next 版本)能力,移植到旧版本 Python 时请改走 PyType_Spec 系列 API。

登录后查看全文
热门项目推荐
相关项目推荐