首页
/ CPython 3.14 C API 移除指南:PyDictObject.ma_version_tag 与不可变类型的可变基类

CPython 3.14 C API 移除指南:PyDictObject.ma_version_tag 与不可变类型的可变基类

2026-09-06 13:25:55作者:咎岭娴Homer

本文基于 CPython 仓库中的弃用说明文档 Doc/deprecations/c-api-pending-removal-in-3.14.rst,系统讲解 Python 3.14 中两项待移除的 C API 变更:PyDictObject 结构体的 ma_version_tag 字段被移除(PEP 699,gh-101193),以及创建带有可变基类的不可变类型(Py_TPFLAGS_IMMUTABLETYPE)被禁止(gh-95388)。读完后,你将了解这两项变更的历史脉络、底层结构布局、替代 API(字典监视器 PyDict_AddWatcher 系列函数)的完整用法,以及扩展模块作者需要的迁移要点。

变更全景:从弃用到移除的两个版本周期

CPython 的 C API 弃用遵循固定的节奏:先在某个版本发出弃用(通常伴随 PyDeprecated 标记或运行时弃用告警),在后续版本正式移除。Doc/deprecations/c-api-pending-removal-in-3.14.rst 原文列出的全部待移除项只有两条:

  1. 扩展模块中 PyDictObjectma_version_tag 字段(PEP 699;gh-101193);
  2. 创建带有可变基类的 Py_TPFLAGS_IMMUTABLETYPE 不可变类型(gh-95388)。

两项都遵循了相同的时间线:Python 3.12 中发出弃用(gh-101193、gh-95388),Python 3.14 中正式移除或收紧为硬错误。Doc/whatsnew/3.14.rst 的 C API 章节对此有对应的移除记录:

* Remove ``PyDictObject.ma_version_tag`` member, which was deprecated
  in Python 3.12.
  Use the :c:func:`PyDict_AddWatcher` API instead.
  (Contributed by Sam Gross in gh-124296.)

以及:

* Creating immutable types with mutable bases was deprecated in
  Python 3.12, and now raises a TypeError.
  (Contributed by Nikita Sobolev in gh-119775.)

下面结合仓库源码逐项展开。

移除 PyDictObject.ma_version_tag:字典版本号机制的终结

历史脉络

ma_version_tag 字段最早由 PEP 509(3.6 引入的函数内联缓存与字节码特化)加入 PyDictObject,用途是让解释器判断全局变量字典等是否发生过修改,从而决定是否使已特化的字节码失效(例如 LOAD_GLOBAL 的特化需要检测 globals/builtins 字典的版本变化)。到了 PEP 699(函数特化重新设计)阶段,这套"整字典版本号"机制被更细粒度的监视器(watcher)机制取代。仓库的发布记录 Misc/NEWS.d/3.14.0a1.rst 明确记录了这一移除:

:c:type:`PyDictObject` no longer maintains a private version tag field
``ma_version_tag`` per PEP 699.  This field was originally added in
Python 3.6 (PEP 509) and deprecated in Python 3.12.

(对应 gh-124296,贡献者 Sam Gross。)

源码证据:3.14 中的 PyDictObject 结构布局

在当前仓库的 Include/cpython/dictobject.h 中,PyDictObject 已不再包含 ma_version_tag,其定义如下:

typedef struct {
    PyObject_HEAD

    /* Number of items in the dictionary */
    Py_ssize_t ma_used;

    /* This is a private field for CPython's internal use.
     * Bits 0-7 are for dict watchers.
     * Bits 8-11 are for the watched mutation counter (used by tier2 optimization)
     * Bits 12-31 are currently unused
     * Bits 32-63 are a unique id in the free threading build (used for per-thread refcounting)
     */
    uint64_t _ma_watcher_tag;

    PyDictKeysObject *ma_keys;

    /* If ma_values is NULL, the table is "combined": keys and values
       are stored in ma_keys.
       If ma_values is not NULL, the table is split:
       keys are stored in ma_keys and values are stored in ma_values */
    PyDictValues *ma_values;
} PyDictObject;

从源码结构看,原来 32 位的 ma_version_taguint32_t)的位置由 64 位的私有字段 _ma_watcher_tag 占据,其位段分配为:

  • 位 0-7:字典监视器(dict watcher)标记;
  • 位 8-11:被监视字典的修改计数器,服务于 tier2 特化优化;
  • 位 12-31:当前未使用;
  • 位 32-63:在 free threading(无 GIL)构建中存放字典唯一 ID,用于每线程引用计数(见 Include/internal/pycore_dict.h 中的 _PyDict_UniqueId)。

这表明该字段并非被简单删除,而是被重新设计为承载监视器机制的复合位域——扩展模块此前对该字段的直接读写方式已不再适用,也不应再适用。

另外值得注意:面向外部 JIT 的键版本查询接口仍然存在,例如 Include/internal/pycore_dict.h 中的 _PyDict_GetKeysVersionForCurrentState,说明"版本"这一概念在 CPython 内部被保留并私有化了,只是不再以 ma_version_tag 的形式暴露给 C API 消费者。

替代方案:PyDict_AddWatcher 字典监视器 API

Doc/whatsnew/3.14.rst 建议的替代方案是 PyDict_AddWatcher API。该组 API 的完整声明位于 Include/cpython/dictobject.h

/* Dictionary watchers */

#define PY_FOREACH_DICT_EVENT(V) \
    V(ADDED)                     \
    V(MODIFIED)                  \
    V(DELETED)                   \
    V(CLONED)                    \
    V(CLEARED)                   \
    V(DEALLOCATED)

typedef enum {
    #define PY_DEF_EVENT(EVENT) PyDict_EVENT_##EVENT,
    PY_FOREACH_DICT_EVENT(PY_DEF_EVENT)
    #undef PY_DEF_EVENT
} PyDict_WatchEvent;

// Callback to be invoked when a watched dict is cleared, dealloced, or modified.
// In clear/dealloc case, key and new_value will be NULL. Otherwise, new_value will be the
// new value for key, NULL if key is being deleted.
typedef int(*PyDict_WatchCallback)(PyDict_WatchEvent event,
                                   PyObject *dict,
                                   PyObject *key,
                                   PyObject *new_value);

// Register/unregister a dict-watcher callback
PyAPI_FUNC(int) PyDict_AddWatcher(PyDict_WatchCallback callback);
PyAPI_FUNC(int) PyDict_ClearWatcher(int watcher_id);

// Mark given dictionary as "watched" (callback will be called if it is modified)
PyAPI_FUNC(int) PyDict_Watch(int watcher_id, PyObject *dict);
PyAPI_FUNC(int) PyDict_Unwatch(int watcher_id, PyObject *dict);

用法要点:

  • 先调用 PyDict_AddWatcher(callback) 注册一个回调,返回的 watcher_id 是后续操作的句柄;
  • 再调用 PyDict_Watch(watcher_id, dict) 把目标字典标记为被监视;字典每次被修改、清空、克隆或释放时,回调都会收到对应的事件(PyDict_EVENT_ADDED / MODIFIED / DELETED / CLONED / CLEARED / DEALLOCATED);
  • 回调签名中,new_value 在删除键时为 NULL,在清空/释放事件中 keynew_value 均为 NULL
  • 不再需要时,用 PyDict_Unwatch 解除监视,用 PyDict_ClearWatcher 注销回调。

从实现侧看,内部通知路径在 Include/internal/pycore_dict.h:每次字典改动会触发 _PyDict_NotifyEvent,它读取 _ma_watcher_tag 的位 0-7 判断是否有监视者挂载,若有则调用 _PyDict_SendEvent 分发给对应的回调。

对依赖 ma_version_tag 的旧扩展(典型场景是自己实现缓存失效检测的模块)而言,迁移路径是:注册一个 watcher,在回调里更新自己的缓存失效标记,取代原先"读版本号 → 与上次比较"的轮询式写法。事件驱动的监视器还能覆盖"字典被克隆"这类旧版本号难以可靠检测的情形。

创建带可变基类的不可变类型:从弃用到 TypeError

背景:Py_TPFLAGS_IMMUTABLETYPE 是什么

Py_TPFLAGS_IMMUTABLETYPE(声明于 Include/object.h)用于标记"类型对象自身不可再修改"的类型。CPython 中静态定义的内置类型(intdict 等)天然具备这一性质:从源码看,Objects/typeobject.ctype_ready 阶段对一切非堆类型自动追加该标志:

/* Historically, all static types were immutable. See bpo-43908 */
if (!(type->tp_flags & Py_TPFLAGS_HEAPTYPE)) {
    type_add_flags(type, Py_TPFLAGS_IMMUTABLETYPE);
    /* Static types must be immortal */
    _Py_SetImmortalUntracked((PyObject *)type);
}

而堆类型(通过 type()PyType_Ready 动态创建的)则只有显式设置该标志时才不可变。这一标志带来一系列约束,例如对不可变类型设置属性会失败:Objects/typeobject.ctype_setattro 直接抛出 "cannot set %R attribute of immutable type '%s'"__annotate____annotations__ 等 setter 也都有同样的检查(见 Objects/typeobject.c)。

3.14 的收紧规则:基类必须全部不可变

不可变类型的语义要求:若类型 A 不可变,其所有基类也必须不可变,否则 MRO 上会出现"上层认为不可变、下层仍可修改"的矛盾状态。Python 3.12 起(gh-95388)这种组合被弃用,3.14 起(gh-119775)升级为 TypeError

仓库源码中有两处校验点,覆盖了类型创建与 MRO 更新两条路径:

  1. 创建时检查Objects/typeobject.c 中的 check_immutable_bases 遍历基类元组,发现任何基类不带 Py_TPFLAGS_IMMUTABLETYPE 即报错:
static int
check_immutable_bases(const char *type_name, PyObject *bases, int skip_first)
{
    Py_ssize_t i = 0;
    if (skip_first) {
        // When testing the MRO, skip the type itself
        i = 1;
    }
    for (; i < PyTuple_GET_SIZE(bases); i++) {
        PyObject *b = PyTuple_GET_ITEM(bases, i);
        if (!b) {
            return -1;
        }
        if (!_PyType_HasFeature(b, Py_TPFLAGS_IMMUTABLETYPE)) {
            PyErr_Format(
                PyExc_TypeError,
                "Creating immutable type %s from mutable base %N",
                type_name, b
            );
            return -1;
        }
    }
    return 0;
}
  1. type_new 入口调用Objects/typeobject.c 中,当传入标志含 Py_TPFLAGS_IMMUTABLETYPE 时立即执行该检查(注释也点明静态类型无此问题,因为只有堆类型才可能是可变的):
/* If this is an immutable type, check if all bases are also immutable.
 * (This isn't necessary for static types: those can't have heap bases,
 * and only heap types can be mutable.)
 */
if (flags & Py_TPFLAGS_IMMUTABLETYPE) {
    if (check_immutable_bases(it.name, bases, 0) < 0) {
        goto finally;
    }
}
  1. MRO 变更路径Objects/typeobject.c 中,类型 MRO 更新(__mro__ 赋值)完成后也会重新调用 check_immutable_bases(type->tp_name, mro, 1)skip_first=1,跳过类型自身),因此运行时动态修改 MRO 引入可变基类同样会被拒绝。

对扩展模块作者的实际影响:如果你的 C 扩展通过 PyType_Ready 或堆类型构造创建标记了 Py_TPFLAGS_IMMUTABLETYPE 的类型(例如 typetyping 相关的 typevarobjectsentinelobject 等模块都使用该标志),其基类链上所有堆类型都必须同样携带该标志,否则在 3.14 上会得到 TypeError: Creating immutable type ... from mutable base ...。3.12/3.13 上这类代码只会收到弃用告警,因此跨版本维护的扩展需要按 3.14 的硬错误标准自查类型层次。

迁移建议与兼容性清单

结合上述两项变更,面向 3.14 的扩展模块迁移可以归纳为:

  1. grep 检查 ma_version_tag:任何直接读取该字段的代码(缓存失效、字典轮询)都需要改写为 PyDict_AddWatcher / PyDict_Watch 事件驱动方案;编译期上,PyDictObject 布局变化意味着偏移量敏感的代码(直接按偏移访问结构体成员)也必须重新编译,不要再依赖旧版头文件的结构尺寸。
  2. 不可变类型自检:确认所有设置 Py_TPFLAGS_IMMUTABLETYPE 的堆类型,其完整 MRO 上不存在可变堆类型基类;注意 3.14 不仅检查创建时,还检查 __mro__ 运行期变更。
  3. 分版本行为差异:这两项在 3.12 与 3.13 中均只是弃用状态(对应 Misc/NEWS.d/3.12.0a5.rst 中的弃用记录),到 3.14 才变为结构移除 / 运行时 TypeError。同时维护多个 Python 版本的扩展,建议以 3.14 的行为为准编写代码,避免在旧版本上依赖已被移除的字段。

参考路径

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
594
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
516
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388