CPython C API Object Protocol 详解:PyObject_* 函数族完整参考与源码级实现剖析
CPython C API Object Protocol 详解PyObject_* 函数族完整参考与源码级实现剖析【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpythonCPython 的 C API 中Object Protocol 是扩展模块开发者接触最频繁的一层接口它把 Python 里hasattr()、o.attr、del o[key]、repr(o)、iter(o)这类日常表达式逐一映射为 C 语言函数并提供了描述符协议、类型检查、哈希与真值判断等底层原语。本文以 CPython 官方文档Doc/c-api/object.rst为骨架完整梳理这套 Object Protocol 的函数族——包括每个函数的签名、返回值语义、错误约定与版本演进——并结合 Objects/object.c 与 Objects/abstract.c 的源码实现解释诸如PyObject_HasAttr静默吞异常的机制、PyObject_GetOptionalAttr针对内置类型的快速路径以及 3.14/3.15 新增的无 GILfree-threaded构建下的引用计数辅助接口。读完本文你将能够准确选型正确的 Object Protocol 函数、理解其引用计数与异常语义并在 C 扩展中安全地访问、修改和比较任意 Python 对象。一、Object Protocol 总览Object Protocol 声明在 Include/object.h实现分散在两个核心文件Objects/object.c常量获取、打印与调试转储、属性访问主入口、PyObject_Repr/PyObject_Str等Objects/abstract.cPyObject_RichCompare、PyObject_Hash、PyObject_IsSubclass、PyObject_IsInstance、PyObject_GetIter等抽象协议函数。它解决的核心问题是一个 C 扩展面对PyObject*时如何不依赖对象的 C 结构体布局仅通过协议与它交互。这与 Python 层面的鸭子类型一脉相承——函数内部通常先取Py_TYPE(o)再调用类型槽tp_getattro、tp_setattro、tp_repr、tp_hash等从而让任意自定义类型都能透明参与。按功能可将其划分为以下家族均完整收录于 Doc/c-api/object.rst功能族主要函数常量获取Py_GetConstant、Py_GetConstantBorrowed打印/调试PyObject_Print、PyObject_Dump、Py_PRINT_RAW属性访问PyObject_HasAttr*、PyObject_GetAttr*、PyObject_GetOptionalAttr*、PyObject_SetAttr*、PyObject_DelAttr*、PyObject_GenericGetAttr/SetAttr__dict__访问PyObject_GenericGetDict、PyObject_GenericSetDict、_PyObject_GetDictPtr比较与真值PyObject_RichCompare、PyObject_RichCompareBool、PyObject_IsTrue、PyObject_Not字符串表示PyObject_Repr、PyObject_Str、PyObject_ASCII、PyObject_Bytes、PyObject_Format类型检查PyObject_IsSubclass、PyObject_IsInstance、PyObject_Type、PyObject_TypeCheck长度与下标PyObject_Size/PyObject_Length、PyObject_LengthHint、PyObject_GetItem/SetItem/DelItem、PyObject_Dir迭代PyObject_GetIter、PyObject_SelfIter、PyObject_GetAIter哈希PyObject_Hash、PyObject_HashNotImplemented类型附加数据PyObject_GetTypeData、PyType_GetTypeDataSize、PyObject_GetItemData受管字典managed dictPyObject_VisitManagedDict、PyObject_ClearManagedDict引用计数3.14/3.15 起PyUnstable 前缀PyUnstable_Object_EnableDeferredRefcount、PyUnstable_Object_IsUniqueReferencedTemporary、PyUnstable_IsImmortal、PyUnstable_TryIncRef、PyUnstable_EnableTryIncRef、PyUnstable_Object_IsUniquelyReferenced、PyUnstable_SetImmortal约定速记贯穿全文返回PyObject*的函数成功时返回新的强引用失败时置异常并返回NULL返回int的函数通常0表示成功、-1表示失败并置异常下标与属性函数不窃取传入值的引用如PyObject_SetItem明确不 stealv的引用。二、常量 APIPy_GetConstant 与 Py_GetConstantBorrowed3.13 引入的Py_GetConstant允许扩展代码以 O(1) 方式获取九个内置常量避免反复构造单例或从None/Py_True等全局变量取值PyObject* Py_GetConstant(unsigned int constant_id);合法的constant_id及对应返回值常量标识符数值返回对象Py_CONSTANT_NONE0NonePy_CONSTANT_FALSE1FalsePy_CONSTANT_TRUE2TruePy_CONSTANT_ELLIPSIS3EllipsisPy_CONSTANT_NOT_IMPLEMENTED4NotImplementedPy_CONSTANT_ZERO50Py_CONSTANT_ONE61Py_CONSTANT_EMPTY_STR7Py_CONSTANT_EMPTY_BYTES8bPy_CONSTANT_EMPTY_TUPLE9()文档特别提醒数值仅在无法使用常量标识符的项目中使用例如受限 API 编译环境新代码应直接使用宏名。若constant_id越界函数置异常并返回NULL。在 CPython 内部这九个常量全部是不朽对象immortal这一点可以从源码直接验证。Objects/object.c 中维护了一张静态表static PyObject* constants[] { _Py_NoneStruct, // Py_CONSTANT_NONE (PyObject*)(_Py_FalseStruct), // Py_CONSTANT_FALSE (PyObject*)(_Py_TrueStruct), // Py_CONSTANT_TRUE _Py_EllipsisObject, // Py_CONSTANT_ELLIPSIS _Py_NotImplementedStruct, // Py_CONSTANT_NOT_IMPLEMENTED NULL, // Py_CONSTANT_ZERO NULL, // Py_CONSTANT_ONE NULL, // Py_CONSTANT_EMPTY_STR NULL, // Py_CONSTANT_EMPTY_BYTES NULL, // Py_CONSTANT_EMPTY_TUPLE };其中0/1//b/()在解释器初始化时由_Py_GetConstant_Init()填充long 与容器常量在 CPython 中同样被设计为 immortal且调试构建下断言每一项都不为NULL且均为 immortal。Py_GetConstant本体只是边界检查加查表见 Objects/object.c。Py_GetConstantBorrowed返回借用引用语义上等价于从解释器借用、在解释器终结前始终有效文档明确指出它主要为向后兼容而保留新代码推荐Py_GetConstant。由于常量本身 immortalCPython 中两者的实现实际相同。与之相关的单例全局变量Py_NotImplemented与宏Py_RETURN_NOTIMPLEMENTED用于在 C 函数中正确返回NotImplemented强引用例如比较类反运算中我不懂这个操作的协议信号。三、打印与调试转储PyObject_Print、Py_PRINT_RAW 与 PyObject_DumpPyObject_Print 与 Py_PRINT_RAWPyObject_Print(PyObject *o, FILE *fp, int flags)将对象打印到文件流失败返回-1。flags目前唯一支持的选项是Py_PRINT_RAW传入时打印str(o)而不是repr(o)该标志同样作用于PyFile_WriteObject等打印函数。PyObject_Dump面向内存损坏的调试转储3.15 新增PyObject_Dump(PyObject *op)把对象转储到stderr仅用于调试。文档强调其输出格式设计目标是在内存可能已损坏的情况下仍尽量能 dump具体策略有四条按最不可能因访问而崩溃的字段优先输出可以在没有 attached thread state 时调用但不推荐可能死锁可以 dump 不属于当前解释器的对象但可能崩溃或行为异常内置启发式判断对象内存是否已被释放——若是只打印内存地址、不访问对象内容输出格式随时可能变化不得依赖。文档给出的示例输出object address : 0x7f80124702c0 object refcount : 2 object type : 0x9902e0 object type name: str object repr : abcdef源码实现与文档描述逐条对应。Objects/object.c 中的启发式函数利用 CPython 内存分配器的调试钩子检测已释放指针int _PyObject_IsFreed(PyObject *op) { if (_PyMem_IsPtrFreed(op) || _PyMem_IsPtrFreed(Py_TYPE(op))) { return 1; } return 0; }而 Objects/object.c 的PyObject_Dump正是先调用_PyObject_IsFreed若已释放则只打印object at %p is freed否则先打印地址与引用计数这两个最安全的字段并fflush再取类型指针与tp_name最后最危险的部分——PyObject_Print调用前临时PyGILState_Ensure并保存/恢复当前异常状态确保即使调用者持有活跃异常或无 GIL 也不会丢失上下文。四、属性访问族HasAttr、GetAttr、SetAttr、DelAttr 与 Optional 变体这是扩展代码中使用频率最高的一组函数也是 3.13/3.15 语义变化最密集的一组。文档将其分成静默版与错误可见版两档选错档位是 C 扩展中吞掉异常或误报错误的常见来源。4.1 静默版PyObject_HasAttr / PyObject_HasAttrStringPyObject_HasAttr(PyObject *o, PyObject *attr_name)与PyObject_HasAttrString属性名为const char*UTF-8 字节串返回1/0永远不失败。文档以 note 明确警告调用__getattr__/__getattribute__过程中抛出的异常不会被传播而是交给sys.unraisablehook如需正确错误处理应改用PyObject_HasAttrWithError、PyObject_GetOptionalAttr或PyObject_GetAttr。源码印证了这一行为。Objects/object.cint PyObject_HasAttr(PyObject *obj, PyObject *name) { int rc PyObject_HasAttrWithError(obj, name); if (rc 0) { PyErr_FormatUnraisable( Exception ignored in PyObject_HasAttr(); consider using PyObject_HasAttrWithError(), PyObject_GetOptionalAttr() or PyObject_GetAttr()); return 0; } return rc; }PyErr_FormatUnraisable会把异常转交给sys.unraisablehook并返回0与文档描述完全一致错误信息里甚至直接给出迁移建议。4.2 错误可见版WithError 变体3.13 新增PyObject_HasAttrWithError与PyObject_HasAttrStringWithError语义与hasattr()等价有属性返回1无属性返回0真实异常时返回-1异常置位由调用者处理。实现上PyObject_HasAttrWithError复用PyObject_GetOptionalAttr后Py_XDECREF掉取到的值见 Objects/object.c。4.3 取值版GetAttr / GetAttrString / GetOptionalAttr / GetOptionalAttrStringPyObject_GetAttr(PyObject *o, PyObject *attr_name)等价o.attr_name成功返回属性值的强引用失败含属性不存在返回NULLPyObject_GetAttrString属性名为const char*版本。3.13 新增的PyObject_GetOptionalAttr填补了一个长期缺口——属性不存在不算错误的场景探测可选钩子、读取可选配置等此前只能用 try/except 模式或PyObject_HasAttrPyObject_GetAttr两次调用存在 TOCTOU 式的重复查找。其返回约定是三值返回值result含义1属性值的新强引用找到0NULL未找到AttributeError被静默-1NULL非AttributeError的其他错误PyObject_GetOptionalAttrString是其const char*版本。源码展示了这个函数的工程细节Objects/object.c 中它对三种 CPython 内置tp_getattro做了类型特化的快速路径if (tp-tp_getattro PyObject_GenericGetAttr) { *result _PyObject_GenericGetAttrWithDict(v, name, NULL, 1); ... } if (tp-tp_getattro _Py_type_getattro) { int suppress_missing_attribute_exception 0; *result _Py_type_getattro_impl((PyTypeObject*)v, name, suppress_missing_attribute_exception); if (suppress_missing_attribute_exception) { // return 0 without having to clear the exception return 0; } } else if (tp-tp_getattro (getattrofunc)_Py_module_getattro) { *result _Py_module_getattro_impl((PyModuleObject*)v, name, 1); ... }即对通用对象、类型对象、模块对象直接调用内部实现并抑制缺失异常省去了先抛AttributeError再PyErr_Clear的开销只有兜底路径才走调用tp_getattro→ 若为AttributeError则PyErr_Clear返回0的通用逻辑。这也解释了为什么PyObject_HasAttrWithError只需一行封装——CPython 自身大量内部代码如 Objects/dictobject.c、Objects/typeobject.c 探测__mro_entries__等都改用这对函数来区分没有该属性与访问出错。4.4 设置与删除SetAttr、DelAttr 及 3.15 的新约束PyObject_SetAttr(PyObject *o, PyObject *attr_name, PyObject *v)等价o.attr_name v成功0、失败-1。当v为NULL时表示删除属性——文档说明该用法已不推荐应改用PyObject_DelAttr但目前没有移除计划。3.15 起的新约束versionchangedPyObject_SetAttr/PyObject_SetAttrString禁止在已置异常状态下传入NULL值调用——这种组合通常源于调用者忘了做NULL检查会静默删掉属性。源码在入口处显式拦截Objects/object.cPyThreadState *tstate _PyThreadState_GET(); if (value NULL _PyErr_Occurred(tstate)) { PyObject *exc _PyErr_GetRaisedException(tstate); _PyErr_SetString(tstate, PyExc_SystemError, PyObject_SetAttr() must not be called with NULL value and an exception set); _PyErr_ChainExceptions1Tstate(tstate, exc); return -1; }注意它把原异常链接进新的SystemError因此旧异常链不丢。PyObject_SetAttrString还有一段性能建议传给它的不同属性名数量应保持很小通常使用静态字符串因为SetAttrString内部会把char*转成str键并可能经PyUnicode_InternFromString路径创建键对象。对于运行期才知道的名字文档建议直接PyUnicode_FromStringPyObject_SetAttr。PyObject_DelAttr/PyObject_DelAttrString等价del o.attr_name失败-1。实现上PyObject_DelAttr就是PyObject_SetAttr(v, name, NULL)见 Objects/object.c。PyObject_SetAttr的另一处实现细节值得注意它会对属性名执行_PyUnicode_InternMortal短生命期内存驻留这也是SetAttrString对键对象走驻留路径的由来。4.5 描述符协议GenericGetAttr 与 GenericSetAttrPyObject_GenericGetAttr(PyObject *o, PyObject *name)是标准实现式的属性读取器设计上放进类型对象的tp_getattro槽沿 MRO 的类字典查找描述符数据描述符优先于实例__dict__非数据描述符次之实例属性最后都没有则抛AttributeError。PyObject_GenericSetAttr(PyObject *o, PyObject *name, PyObject *value)是对应的设置/删除器放进tp_setattro槽优先 MRO 类字典中的数据描述符否则在实例__dict__中设置或删除无__dict__等失败情况抛AttributeError并返回-1。自定义 C 类型若不覆写tp_getattro/tp_setattro就能自动获得与 Python 类一致的属性语义。五、dict访问GenericGetDict、GenericSetDict 与 _PyObject_GetDictPtrPyObject_GenericGetDict(PyObject *o, void *context)__dict__描述符的通用 getter 实现必要时创建字典作为直接调用获取o.__dict__时context传NULL。文档提醒由于它可能为创建字典分配内存若目的只是访问某个属性调用PyObject_GetAttr可能更高效。失败返回NULL并置异常。PyObject_GenericSetDict(PyObject *o, PyObject *value, void *context)__dict__描述符的通用 setter不允许删除字典。_PyObject_GetDictPtr(PyObject *obj)内部 API下划线前缀返回对象__dict__的指针PyObject**对象没有__dict__时返回NULL且不置异常。同样地它可能分配内存文档给出与上条相同的性能提示。三者都是自 3.3 起随tp_dictoffset语义一起提供的标准组件用于让 C 类型拥有 Python 式的实例字典。六、比较、真值与格式化PyObject_RichCompare 与 PyObject_RichCompareBoolPyObject_RichCompare(PyObject *o1, PyObject *o2, int opid)执行o1 op o2opid必须为Py_LT/Py_LE/Py_EQ/Py_NE/Py_GT/Py_GE对应、、、!、、成功返回比较结果的新强引用失败返回NULL。PyObject_RichCompareBool只关心布尔结果-1错误、0假、1真。文档附有一条重要 note当o1与o2是同一对象时Py_EQ恒返回1、Py_NE恒返回0——这是引用同一性的短路优化意味着is级别的比较不经过__eq__。真值判断PyObject_IsTrue等价not not o1真 /0假 /-1失败PyObject_Not等价not o注意返回语义反转0表示对象为真失败-1。格式化与字符串表示PyObject_Format(PyObject *obj, PyObject *format_spec)等价format(obj, format_spec)format_spec允许为NULL等价format(obj)。成功返回格式化字符串失败NULL。PyObject_Repr(PyObject *o)等价repr(o)是内建repr()的底层实现。文档特别规定参数为NULL时返回字符串NULL。3.4 起加入调试断言防止在存在活跃异常时被静默调用而丢失异常。源码可见其实现开头Objects/object.c先PyErr_CheckSignals()、再处理NULL分支类型无tp_repr时回退为%s object at %p形式。PyObject_Str(PyObject *o)等价str(o)内建str()与print()的底层NULL参数同样返回NULL3.4 起有同样的活跃异常断言。PyObject_ASCII(PyObject *o)等价ascii()在repr结果上把非 ASCII 字符转义为\x/\u/\U形式NULL参数返回NULL。PyObject_Bytes(PyObject *o)等价bytes(o)当o非整数时差异在于整数参数抛TypeError而不是返回零填充 bytes 对象。NULL参数返回bNULL。七、类型检查与对象元信息PyObject_IsSubclass 与 PyObject_IsInstancePyObject_IsSubclass(PyObject *derived, PyObject *cls)derived与cls相同或derived派生自cls时返回1否则0出错-1。要点cls可以是元组对每个条目检查任一成立即为1等价isinstance/issubclass的元组形式若cls定义了__subclasscheck__PEP 3119 的 ABC 机制以该方法判定否则检查derived是否在cls.__mro__中通常只有type或其子类实例才算类但对象可通过定义__bases__属性基类元组来自行声明自己是类。PyObject_IsInstance(PyObject *inst, PyObject *cls)inst是cls或子类实例时返回1否则0出错-1并置异常。同样支持cls为元组、__instancecheck__钩子PEP 3119、__class__覆盖实例类型、__bases__覆盖类判定。PyObject_Type 与 PyObject_TypeCheckPyObject_Type(PyObject *o)等价type(o)返回类型对象的新强引用失败抛SystemError并返回NULL。文档直言除非你需要强引用否则应使用零开销的Py_TYPE()宏而不是此函数。PyObject_TypeCheck(PyObject *o, PyTypeObject *type)o是该类型或其子类型时返回非零否则0两个参数均不得为NULL。长度与下标访问PyObject_Size别名PyObject_Length等价len(o)若对象同时提供序列与映射协议返回序列长度错误-1。PyObject_LengthHint(PyObject *o, Py_ssize_t defaultvalue)3.4先取真实长度失败则尝试__length_hint__再失败返回defaultvalue错误返回-1。等价operator.length_hint(o, defaultvalue)常用于容器预分配容量。PyObject_GetItem(PyObject *o, PyObject *key)等价o[key]失败NULL。PyObject_SetItem(PyObject *o, PyObject *key, PyObject *v)等价o[key] v成功0/失败-1文档强调不窃取v的引用。PyObject_DelItem/PyObject_DelItemString等价del o[key]失败-1后者接受const char*键。PyObject_Dir(PyObject *o)等价dir(o)返回字符串列表可能为空错误NULL参数为NULL时类似 Python 的dir()——返回当前局部变量名若无活动帧则返回NULL且PyErr_Occurred为假。哈希PyObject_Hash(PyObject *o)等价hash(o)失败返回-1。3.2 起返回类型Py_hash_t与Py_ssize_t同宽的有符号整数——注意哈希值本身可以是-1与错误同值是调用者需自行处理的经典歧义。PyObject_HashNotImplemented(PyObject *o)设置type(o)不可哈希的TypeError并返回-1把它放进tp_hash槽时会被解释器特殊对待用于显式声明类型不可哈希。八、迭代器协议PyObject_GetIter(PyObject *o)等价iter(o)返回新迭代器若o本身已是迭代器则返回其自身不可迭代时抛TypeError并返回NULL。PyObject_SelfIter(PyObject *obj)等价def __iter__(self): return self供迭代器类型直接填入tp_iter槽。PyObject_GetAIter(PyObject *o)3.10等价aiter(o)对AsyncIterable返回AsyncIterator若参数已是AsyncIterator则返回自身不可异步迭代时抛TypeError。九、类型附加数据与受管字典3.12/3.13 新增负数 basicsize 与 PyType_GetTypeDataCPython 3.12 允许PyType_Spec.basicsize取负值来请求由解释器管理的子类专属附加数据区配套 API 有void *PyObject_GetTypeData(PyObject *o, PyTypeObject *cls)取o上为cls保留的数据区指针。要求o是cls的实例且cls必须以负basicsize创建——Python 不做这些检查即契约完全由调用者保证。出错置异常返回NULL。Py_ssize_t PyType_GetTypeDataSize(PyTypeObject *cls)返回实际保留的数据区大小可能大于请求值文档明确这个更大尺寸可以安全使用例如配合memset清零类型必须以负basicsize创建否则行为未定义。出错返回负值。Py_TPFLAGS_ITEMS_AT_END 与 PyObject_GetItemDatavoid *PyObject_GetItemData(PyObject *o)3.12获取带Py_TPFLAGS_ITEMS_AT_END标志的类中每个实例对象尾部的 per-item 数据区指针o没有该标志时抛TypeError。这一机制让 C 类型的实例数据以固定偏移的尾部区域形式存在配合 3.12 引入的PyType_Ready布局策略使用。受管字典managed dictint PyObject_VisitManagedDict(PyObject *obj, visitproc visit, void *arg)3.13遍历对象的受管字典只能在设置了Py_TPFLAGS_MANAGED_DICT的类型的 traverse 函数中调用。void PyObject_ClearManagedDict(PyObject *obj)3.13清空受管字典只能在相应类型的 clear 函数中调用。受管字典把__dict__从实例内嵌的指针传统tp_dictoffset模式改为解释器侧管理的堆字典减少了每个实例 8 字节的指针槽是 CPython 内存布局优化的一部分扩展类型只需在tp_traverse/tp_clear中用这两个函数即可正确参与 GC。十、free-threading 时代的引用计数辅助PyUnstable 系列3.14/3.15 引入了一批PyUnstable_前缀的函数服务于无 GILfree-threaded构建下的高性能扩展开发。带PyUnstable_前缀意味着接口可能在 3.15 之前变化但它们是官方公开的 C API 而非内部接口。延迟引用计数PyUnstable_Object_EnableDeferredRefcount3.14在受支持的运行时上为obj开启延迟引用计数deferred reference counting解释器此后对该对象不再做引用计数调整多线程场景下可减少锁竞争、提升性能代价是对象只能被追踪型 GC 回收而不再在最后一个引用消失时立即释放。返回1表示成功开启0表示不支持或提示被忽略例如已经开启。函数线程安全、不会失败。在 GIL 构建上该函数是空操作对象若不受 GC 追踪见gc.is_tracked/PyObject_GC_IsTracked同样无效。文档建议的调用时机是对象刚创建时如tp_new槽中。唯一临时对象判定PyUnstable_Object_IsUniqueReferencedTemporary3.14返回1当obj已知是唯一临时对象当前代码持有其唯一引用检查是保守的可能低估返回0。文档用两个例子划清边界my_func([1, 2, 3]) # 参数是唯一临时对象 my_list [1, 2, 3] my_func(my_list) # 即使 refcount 为 1也不是唯一临时对象关键背景自 3.14 起解释器在把对象加载到操作数栈时会尽可能借用引用因此引用计数为 1本身不再能唯一引用性下结论——C 函数参数是否独享应改用此函数而非Py_REFCNT(x) 1。不朽对象PyUnstable_IsImmortal 与 PyUnstable_SetImmortalPyUnstable_IsImmortal(PyObject *obj)3.14非零表示obj是 immortal不会失败。note 提醒某对象在某个 CPython 版本中 immortal不保证在另一版本中仍是。PyUnstable_SetImmortal(PyObject *op)3.15把op标记为 immortal参数应被调用线程唯一引用面向跨线程共享对象以减少 free-threaded 构建下的引用计数竞争。这是单向操作对象只能变为 immortal不能逆转immortal 对象不参与引用计数、永不被 GC 回收若原对象受 GC 追踪则会被 untrack。返回1/0不会失败。同样建议在tp_new等创建路径中尽早调用。原子 TryIncRef 与轻量弱引用PyUnstable_TryIncRef / PyUnstable_EnableTryIncRef3.14PyUnstable_TryIncRef(PyObject *obj)若引用计数非零则原子地自增并返回1否则返回0。逻辑上等价于if (Py_REFCNT(op) 0) { Py_INCREF(op); return 1; } return 0;区别在于 free-threaded 构建中它是原子的check-then-inc 不产生竞争窗口。前置条件是之前对该对象调用过PyUnstable_EnableTryIncRef调用者须持有强引用否则在 free-threaded 构建中可能错误地返回0。文档给出的典型用途是不依赖 Python 弱引用对象实现弱引用——官方示例是一个针对特定类型的 weakmap骨架如下摘自 Doc/c-api/object.rstPyMutex mutex; PyObject * add_entry(weakmap_key_type *key, PyObject *value) { PyUnstable_EnableTryIncRef(value); weakmap_type weakmap ...; PyMutex_Lock(mutex); weakmap_add_entry(weakmap, key, value); PyMutex_Unlock(mutex); Py_RETURN_NONE; } PyObject * get_value(weakmap_key_type *key) { weakmap_type weakmap ...; PyMutex_Lock(mutex); PyObject *result weakmap_find(weakmap, key); if (PyUnstable_TryIncRef(result)) { // result is safe to use PyMutex_Unlock(mutex); return result; } // if we get here, result is starting to be garbage-collected, // but has not been removed from the weakmap yet PyMutex_Unlock(mutex); return NULL; } // tp_dealloc function for weakmap values void value_dealloc(PyObject *value) { weakmap_type weakmap ...; PyMutex_Lock(mutex); weakmap_remove_value(weakmap, value); ... PyMutex_Unlock(mutex); }该模式成立的关键是三重配合EnableTryIncRef在存入时声明意图TryIncRef在读取时原子胜出才说明对象仍存活而正确性最终依赖对象tp_dealloc中把自身从表中摘除——文档特别强调这一点通常需要obj析构函数的配合。PyUnstable_Object_IsUniquelyReferenced3.14int PyUnstable_Object_IsUniquelyReferenced(PyObject *op)判断op是否只有唯一引用。在 GIL 构建中等价于Py_REFCNT(op) 1在 free-threaded 构建中除了检查引用计数为一还检查op仅被当前线程使用——文档明确警告Py_REFCNT(op) 1在无 GIL 构建下不是线程安全的应优先用本函数。注意尽管它不进入解释器内部逻辑调用者仍必须持有 attached thread state函数不会失败。十一、版本演进速查与选型建议将 Doc/c-api/object.rst 中的versionadded/versionchanged标注汇总版本变化3.2PyObject_Hash返回类型改为Py_hash_t3.3PyObject_GenericGetDict/PyObject_GenericSetDict引入3.4PyObject_LengthHint引入PyObject_Repr/PyObject_Str加入活跃异常调试断言3.10PyObject_GetAIter引入3.12PyObject_GetTypeData、PyType_GetTypeDataSize、PyObject_GetItemData引入3.13Py_GetConstant、Py_GetConstantBorrowed、PyObject_HasAttrWithError、PyObject_HasAttrStringWithError、PyObject_GetOptionalAttr、PyObject_GetOptionalAttrString、PyObject_VisitManagedDict、PyObject_ClearManagedDict引入3.14PyUnstable_Object_EnableDeferredRefcount、PyUnstable_Object_IsUniqueReferencedTemporary、PyUnstable_IsImmortal、PyUnstable_TryIncRef、PyUnstable_EnableTryIncRef、PyUnstable_Object_IsUniquelyReferenced引入3.15PyObject_Dump、PyUnstable_SetImmortal引入PyObject_SetAttr/PyObject_SetAttrString增加异常已置位时不得以 NULL 值调用的约束基于上述语义的选型建议属性探测若缺失是正常情况用PyObject_GetOptionalAttr(String)一次调用、无重复查找若必须区分缺失与出错用PyObject_HasAttr*WithError只有明确不关心异常、且能接受 unraisable hook 路径时才用PyObject_HasAttr*。写属性删除一律用PyObject_DelAttr(String)不要依赖SetAttr传NULL的旧行为升级/支持 3.15 后注意异常置位 NULL 值会被显式拒绝为SystemError。取常量新代码统一Py_GetConstant避免Py_INCREF(Py_None)之类的惯用法。free-threaded 扩展判断参数是否独享引用用PyUnstable_Object_IsUniqueReferencedTemporary/PyUnstable_Object_IsUniquelyReferenced热路径对象在tp_new中考虑PyUnstable_Object_EnableDeferredRefcount注意PyUnstable_前缀接口的稳定性契约。C 类型的属性语义不覆写tp_getattro/tp_setattro时默认落到PyObject_GenericGetAttr/PyObject_GenericSetAttr语义数据描述符 实例字典自写描述符时应与这一优先级对齐。十二、小结CPython 的 Object Protocol 是 C 扩展与 Python 对象世界之间的通用协议层Doc/c-api/object.rst 定义的每个函数都把一条 Python 表达式语义o.attr、del o[key]、repr(o)、format(o, spec)……固化为具有精确引用计数与异常契约的 C 入口Objects/object.c 与 Objects/abstract.c 的实现进一步揭示了 CPython 在其中的工程权衡——GetOptionalAttr的内置类型快速路径、HasAttr的 unraisable 降级、SetAttr的异常置位防护、PyObject_Dump的释放内存启发式都是协议语义 性能/健壮性两全的产物。理解这层协议及其版本演进是编写正确、高效 C 扩展的基础。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考