ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

CPython 3.14 C API 移除指南:PyDictObject.ma_version_tag 与不可变类型的可变基类 CPython 3.14 C API 移除指南PyDictObject.ma_version_tag 与不可变类型的可变基类【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython本文基于 CPython 仓库中的弃用说明文档 Doc/deprecations/c-api-pending-removal-in-3.14.rst系统讲解 Python 3.14 中两项待移除的 C API 变更PyDictObject结构体的ma_version_tag字段被移除PEP 699gh-101193以及创建带有可变基类的不可变类型Py_TPFLAGS_IMMUTABLETYPE被禁止gh-95388。读完后你将了解这两项变更的历史脉络、底层结构布局、替代 API字典监视器PyDict_AddWatcher系列函数的完整用法以及扩展模块作者需要的迁移要点。变更全景从弃用到移除的两个版本周期CPython 的 C API 弃用遵循固定的节奏先在某个版本发出弃用通常伴随PyDeprecated标记或运行时弃用告警在后续版本正式移除。Doc/deprecations/c-api-pending-removal-in-3.14.rst 原文列出的全部待移除项只有两条扩展模块中PyDictObject的ma_version_tag字段PEP 699gh-101193创建带有可变基类的Py_TPFLAGS_IMMUTABLETYPE不可变类型gh-95388。两项都遵循了相同的时间线Python 3.12 中发出弃用gh-101193、gh-95388Python 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 5093.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 CPythons 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 字典监视器 APIDoc/whatsnew/3.14.rst 建议的替代方案是PyDict_AddWatcherAPI。该组 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在清空/释放事件中key和new_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 中静态定义的内置类型int、dict等天然具备这一性质从源码看Objects/typeobject.c 中type_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.c 的type_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 更新两条路径创建时检查。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; }type_new 入口调用。Objects/typeobject.c 中当传入标志含Py_TPFLAGS_IMMUTABLETYPE时立即执行该检查注释也点明静态类型无此问题因为只有堆类型才可能是可变的/* If this is an immutable type, check if all bases are also immutable. * (This isnt necessary for static types: those cant 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; } }MRO 变更路径。Objects/typeobject.c 中类型 MRO 更新__mro__赋值完成后也会重新调用check_immutable_bases(type-tp_name, mro, 1)skip_first1跳过类型自身因此运行时动态修改 MRO 引入可变基类同样会被拒绝。对扩展模块作者的实际影响如果你的 C 扩展通过PyType_Ready或堆类型构造创建标记了Py_TPFLAGS_IMMUTABLETYPE的类型例如type、typing相关的typevarobject、sentinelobject等模块都使用该标志其基类链上所有堆类型都必须同样携带该标志否则在 3.14 上会得到TypeError: Creating immutable type ... from mutable base ...。3.12/3.13 上这类代码只会收到弃用告警因此跨版本维护的扩展需要按 3.14 的硬错误标准自查类型层次。迁移建议与兼容性清单结合上述两项变更面向 3.14 的扩展模块迁移可以归纳为grep 检查ma_version_tag任何直接读取该字段的代码缓存失效、字典轮询都需要改写为PyDict_AddWatcher/PyDict_Watch事件驱动方案编译期上PyDictObject布局变化意味着偏移量敏感的代码直接按偏移访问结构体成员也必须重新编译不要再依赖旧版头文件的结构尺寸。不可变类型自检确认所有设置Py_TPFLAGS_IMMUTABLETYPE的堆类型其完整 MRO 上不存在可变堆类型基类注意 3.14 不仅检查创建时还检查__mro__运行期变更。分版本行为差异这两项在 3.12 与 3.13 中均只是弃用状态对应 Misc/NEWS.d/3.12.0a5.rst 中的弃用记录到 3.14 才变为结构移除 / 运行时TypeError。同时维护多个 Python 版本的扩展建议以 3.14 的行为为准编写代码避免在旧版本上依赖已被移除的字段。参考路径弃用说明原文Doc/deprecations/c-api-pending-removal-in-3.14.rst同目录还有 c-api-pending-removal-in-3.15.rst、c-api-pending-removal-in-3.16.rst 等后续版本的移除清单以及 soft-deprecations.rst结构体与监视器 APIInclude/cpython/dictobject.h内部字典实现与事件分发Include/internal/pycore_dict.h类型不可变标志检查逻辑Objects/typeobject.c3.14 版本说明Doc/whatsnew/3.14.rst移除记录Misc/NEWS.d/3.14.0a1.rst【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进