首页
/ CPython C API Object Protocol 详解:PyObject_* 函数族完整参考与源码级实现剖析

CPython C API Object Protocol 详解:PyObject_* 函数族完整参考与源码级实现剖析

2026-09-04 15:36:30作者:申梦珏Efrain

CPython 的 C API 中,Object Protocol 是扩展模块开发者接触最频繁的一层接口:它把 Python 里 hasattr()o.attrdel o[key]repr(o)iter(o) 这类日常表达式,逐一映射为 C 语言函数,并提供了描述符协议、类型检查、哈希与真值判断等底层原语。本文以 CPython 官方文档 Doc/c-api/object.rst 为骨架,完整梳理这套 Object Protocol 的函数族——包括每个函数的签名、返回值语义、错误约定与版本演进——并结合 Objects/object.cObjects/abstract.c 的源码实现,解释诸如 PyObject_HasAttr 静默吞异常的机制、PyObject_GetOptionalAttr 针对内置类型的快速路径,以及 3.14/3.15 新增的无 GIL(free-threaded)构建下的引用计数辅助接口。读完本文,你将能够准确选型正确的 Object Protocol 函数、理解其引用计数与异常语义,并在 C 扩展中安全地访问、修改和比较任意 Python 对象。

一、Object Protocol 总览

Object Protocol 声明在 Include/object.h,实现分散在两个核心文件:

  • Objects/object.c:常量获取、打印与调试转储、属性访问主入口、PyObject_Repr/PyObject_Str 等;
  • Objects/abstract.cPyObject_RichComparePyObject_HashPyObject_IsSubclassPyObject_IsInstancePyObject_GetIter 等抽象协议函数。

它解决的核心问题是:一个 C 扩展面对 PyObject* 时,如何不依赖对象的 C 结构体布局,仅通过"协议"与它交互。这与 Python 层面的鸭子类型一脉相承——函数内部通常先取 Py_TYPE(o),再调用类型槽(tp_getattrotp_setattrotp_reprtp_hash 等),从而让任意自定义类型都能透明参与。

按功能可将其划分为以下家族,均完整收录于 Doc/c-api/object.rst

功能族 主要函数
常量获取 Py_GetConstantPy_GetConstantBorrowed
打印/调试 PyObject_PrintPyObject_DumpPy_PRINT_RAW
属性访问 PyObject_HasAttr*PyObject_GetAttr*PyObject_GetOptionalAttr*PyObject_SetAttr*PyObject_DelAttr*PyObject_GenericGetAttr/SetAttr
__dict__ 访问 PyObject_GenericGetDictPyObject_GenericSetDict_PyObject_GetDictPtr
比较与真值 PyObject_RichComparePyObject_RichCompareBoolPyObject_IsTruePyObject_Not
字符串表示 PyObject_ReprPyObject_StrPyObject_ASCIIPyObject_BytesPyObject_Format
类型检查 PyObject_IsSubclassPyObject_IsInstancePyObject_TypePyObject_TypeCheck
长度与下标 PyObject_Size/PyObject_LengthPyObject_LengthHintPyObject_GetItem/SetItem/DelItemPyObject_Dir
迭代 PyObject_GetIterPyObject_SelfIterPyObject_GetAIter
哈希 PyObject_HashPyObject_HashNotImplemented
类型附加数据 PyObject_GetTypeDataPyType_GetTypeDataSizePyObject_GetItemData
受管字典(managed dict) PyObject_VisitManagedDictPyObject_ClearManagedDict
引用计数(3.14/3.15 起,PyUnstable 前缀) PyUnstable_Object_EnableDeferredRefcountPyUnstable_Object_IsUniqueReferencedTemporaryPyUnstable_IsImmortalPyUnstable_TryIncRefPyUnstable_EnableTryIncRefPyUnstable_Object_IsUniquelyReferencedPyUnstable_SetImmortal

约定速记(贯穿全文):返回 PyObject* 的函数成功时返回新的强引用,失败时置异常并返回 NULL;返回 int 的函数通常 0 表示成功、-1 表示失败并置异常;下标与属性函数不"窃取"传入值的引用(如 PyObject_SetItem 明确不 steal v 的引用)。

二、常量 API:Py_GetConstant 与 Py_GetConstantBorrowed

3.13 引入的 Py_GetConstant 允许扩展代码以 O(1) 方式获取九个内置常量,避免反复构造单例或从 None/Py_True 等全局变量取值:

PyObject* Py_GetConstant(unsigned int constant_id);

合法的 constant_id 及对应返回值:

常量标识符 数值 返回对象
Py_CONSTANT_NONE 0 None
Py_CONSTANT_FALSE 1 False
Py_CONSTANT_TRUE 2 True
Py_CONSTANT_ELLIPSIS 3 Ellipsis
Py_CONSTANT_NOT_IMPLEMENTED 4 NotImplemented
Py_CONSTANT_ZERO 5 0
Py_CONSTANT_ONE 6 1
Py_CONSTANT_EMPTY_STR 7 ''
Py_CONSTANT_EMPTY_BYTES 8 b''
Py_CONSTANT_EMPTY_TUPLE 9 ()

文档特别提醒:数值仅在无法使用常量标识符的项目中使用(例如受限 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。由于常量本身 immortal,CPython 中两者的实现实际相同。

与之相关的单例全局变量 Py_NotImplemented 与宏 Py_RETURN_NOTIMPLEMENTED 用于在 C 函数中正确返回 NotImplemented 强引用(例如比较类反运算中"我不懂这个操作"的协议信号)。

三、打印与调试转储:PyObject_Print、Py_PRINT_RAW 与 PyObject_Dump

PyObject_Print 与 Py_PRINT_RAW

PyObject_Print(PyObject *o, FILE *fp, int flags) 将对象打印到文件流,失败返回 -1flags 目前唯一支持的选项是 Py_PRINT_RAW:传入时打印 str(o) 而不是 repr(o),该标志同样作用于 PyFile_WriteObject 等打印函数。

PyObject_Dump:面向内存损坏的调试转储(3.15 新增)

PyObject_Dump(PyObject *op) 把对象转储到 stderr仅用于调试。文档强调其输出格式设计目标是"在内存可能已损坏的情况下仍尽量能 dump",具体策略有四条:

  1. 按"最不可能因访问而崩溃的字段"优先输出;
  2. 可以在没有 attached thread state 时调用,但不推荐(可能死锁);
  3. 可以 dump 不属于当前解释器的对象,但可能崩溃或行为异常;
  4. 内置启发式判断对象内存是否已被释放——若是,只打印内存地址、不访问对象内容;
  5. 输出格式随时可能变化,不得依赖。

文档给出的示例输出:

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.cPyObject_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_HasAttrString

PyObject_HasAttr(PyObject *o, PyObject *attr_name)PyObject_HasAttrString(属性名为 const char* UTF-8 字节串)返回 1/0永远不失败。文档以 note 明确警告:调用 __getattr__/__getattribute__ 过程中抛出的异常不会被传播,而是交给 sys.unraisablehook;如需正确错误处理,应改用 PyObject_HasAttrWithErrorPyObject_GetOptionalAttrPyObject_GetAttr

源码印证了这一行为。Objects/object.c

int
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_HasAttrWithErrorPyObject_HasAttrStringWithError 语义与 hasattr() 等价:有属性返回 1,无属性返回 0真实异常时返回 -1(异常置位,由调用者处理)。实现上,PyObject_HasAttrWithError 复用 PyObject_GetOptionalAttrPy_XDECREF 掉取到的值(见 Objects/object.c)。

4.3 取值版:GetAttr / GetAttrString / GetOptionalAttr / GetOptionalAttrString

  • PyObject_GetAttr(PyObject *o, PyObject *attr_name):等价 o.attr_name,成功返回属性值的强引用,失败(含属性不存在)返回 NULL
  • PyObject_GetAttrString:属性名为 const char* 版本。

3.13 新增的 PyObject_GetOptionalAttr 填补了一个长期缺口——"属性不存在不算错误"的场景(探测可选钩子、读取可选配置等)此前只能用 try/except 模式或 PyObject_HasAttr + PyObject_GetAttr 两次调用(存在 TOCTOU 式的重复查找)。其返回约定是三值:

返回值 result 含义
1 属性值的新强引用 找到
0 NULL 未找到,AttributeError 被静默
-1 NULL 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);
    ...
}

即:对通用对象、类型对象、模块对象,直接调用内部实现并"抑制缺失异常",省去了先抛 AttributeErrorPyErr_Clear 的开销;只有兜底路径才走"调用 tp_getattro → 若为 AttributeErrorPyErr_Clear 返回 0"的通用逻辑。这也解释了为什么 PyObject_HasAttrWithError 只需一行封装——CPython 自身大量内部代码(如 Objects/dictobject.cObjects/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。当 vNULL 时表示删除属性——文档说明该用法已不推荐(应改用 PyObject_DelAttr),但"目前没有移除计划"。
  • 3.15 起的新约束(versionchanged):PyObject_SetAttr/PyObject_SetAttrString 禁止在"已置异常"状态下传入 NULL 值调用——这种组合通常源于调用者忘了做 NULL 检查,会静默删掉属性。源码在入口处显式拦截(Objects/object.c):
PyThreadState *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_FromString + PyObject_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 与 GenericSetAttr

PyObject_GenericGetAttr(PyObject *o, PyObject *name) 是"标准实现"式的属性读取器,设计上放进类型对象的 tp_getattro 槽:

  1. 沿 MRO 的类字典查找描述符
  2. 数据描述符优先于实例 __dict__,非数据描述符次之,实例属性最后;
  3. 都没有则抛 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_GetDictPtr

  • PyObject_GenericGetDict(PyObject *o, void *context)__dict__ 描述符的通用 getter 实现,必要时创建字典;作为直接调用获取 o.__dict__contextNULL。文档提醒:由于它可能为创建字典分配内存,若目的只是访问某个属性,调用 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_RichCompareBool

PyObject_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:o1o2 是同一对象时,Py_EQ 恒返回 1Py_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 参数同样返回 '<NULL>',3.4 起有同样的活跃异常断言。
  • PyObject_ASCII(PyObject *o):等价 ascii(),在 repr 结果上把非 ASCII 字符转义为 \x/\u/\U 形式;NULL 参数返回 '<NULL>'
  • PyObject_Bytes(PyObject *o):等价 bytes(o)(当 o 非整数时);差异在于整数参数抛 TypeError 而不是返回零填充 bytes 对象。NULL 参数返回 b'<NULL>'

七、类型检查与对象元信息

PyObject_IsSubclass 与 PyObject_IsInstance

PyObject_IsSubclass(PyObject *derived, PyObject *cls)derivedcls 相同或 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)instcls 或子类实例时返回 1,否则 0,出错 -1 并置异常。同样支持 cls 为元组、__instancecheck__ 钩子(PEP 3119)、__class__ 覆盖实例类型、__bases__ 覆盖类判定。

PyObject_Type 与 PyObject_TypeCheck

  • PyObject_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()——返回当前局部变量名,若无活动帧则返回 NULLPyErr_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_GetTypeData

CPython 3.12 允许 PyType_Spec.basicsize负值来请求由解释器管理的"子类专属附加数据区",配套 API 有:

  • void *PyObject_GetTypeData(PyObject *o, PyTypeObject *cls):取 o 上为 cls 保留的数据区指针。要求 ocls 的实例,且 cls 必须以负 basicsize 创建——Python 不做这些检查(即契约完全由调用者保证)。出错置异常返回 NULL
  • Py_ssize_t PyType_GetTypeDataSize(PyTypeObject *cls):返回实际保留的数据区大小,可能大于请求值,文档明确这个更大尺寸可以安全使用(例如配合 memset 清零);类型必须以负 basicsize 创建,否则行为未定义。出错返回负值。

Py_TPFLAGS_ITEMS_AT_END 与 PyObject_GetItemData

void *PyObject_GetItemData(PyObject *o)(3.12+):获取带 Py_TPFLAGS_ITEMS_AT_END 标志的类中,每个实例对象尾部的 per-item 数据区指针;o 没有该标志时抛 TypeError。这一机制让 C 类型的实例数据以"固定偏移的尾部区域"形式存在,配合 3.12 引入的 PyType_Ready 布局策略使用。

受管字典(managed dict)

  • int 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_ 前缀的函数,服务于无 GIL(free-threaded)构建下的高性能扩展开发。带 PyUnstable_ 前缀意味着接口可能在 3.15 之前变化,但它们是官方公开的 C API 而非内部接口。

延迟引用计数:PyUnstable_Object_EnableDeferredRefcount(3.14+)

在受支持的运行时上为 obj 开启延迟引用计数(deferred reference counting):解释器此后对该对象不再做引用计数调整,多线程场景下可减少锁竞争、提升性能;代价是对象只能被追踪型 GC 回收,而不再在"最后一个引用消失"时立即释放。返回 1 表示成功开启;0 表示不支持或提示被忽略(例如已经开启)。函数线程安全、不会失败。在 GIL 构建上该函数是空操作;对象若不受 GC 追踪(见 gc.is_tracked / PyObject_GC_IsTracked)同样无效。文档建议的调用时机是对象刚创建时(如 tp_new 槽中)。

唯一临时对象判定:PyUnstable_Object_IsUniqueReferencedTemporary(3.14+)

返回 1obj 已知是唯一临时对象(当前代码持有其唯一引用),检查是保守的,可能低估(返回 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_SetImmortal

  • PyUnstable_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_EnableTryIncRef(3.14+)

PyUnstable_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.rst):

PyMutex 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_IsUniquelyReferenced(3.14+)

int 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.2 PyObject_Hash 返回类型改为 Py_hash_t
3.3 PyObject_GenericGetDict/PyObject_GenericSetDict 引入
3.4 PyObject_LengthHint 引入;PyObject_Repr/PyObject_Str 加入活跃异常调试断言
3.10 PyObject_GetAIter 引入
3.12 PyObject_GetTypeDataPyType_GetTypeDataSizePyObject_GetItemData 引入
3.13 Py_GetConstantPy_GetConstantBorrowedPyObject_HasAttrWithErrorPyObject_HasAttrStringWithErrorPyObject_GetOptionalAttrPyObject_GetOptionalAttrStringPyObject_VisitManagedDictPyObject_ClearManagedDict 引入
3.14 PyUnstable_Object_EnableDeferredRefcountPyUnstable_Object_IsUniqueReferencedTemporaryPyUnstable_IsImmortalPyUnstable_TryIncRefPyUnstable_EnableTryIncRefPyUnstable_Object_IsUniquelyReferenced 引入
3.15 PyObject_DumpPyUnstable_SetImmortal 引入;PyObject_SetAttr/PyObject_SetAttrString 增加"异常已置位时不得以 NULL 值调用"的约束

基于上述语义的选型建议:

  1. 属性探测:若"缺失"是正常情况,用 PyObject_GetOptionalAttr(String)(一次调用、无重复查找);若必须区分"缺失"与"出错",用 PyObject_HasAttr*WithError;只有明确不关心异常、且能接受 unraisable hook 路径时才用 PyObject_HasAttr*
  2. 写属性:删除一律用 PyObject_DelAttr(String),不要依赖 SetAttrNULL 的旧行为;升级/支持 3.15 后注意"异常置位 + NULL 值"会被显式拒绝为 SystemError
  3. 取常量:新代码统一 Py_GetConstant,避免 Py_INCREF(Py_None) 之类的惯用法。
  4. free-threaded 扩展:判断参数是否独享引用用 PyUnstable_Object_IsUniqueReferencedTemporary/PyUnstable_Object_IsUniquelyReferenced,热路径对象在 tp_new 中考虑 PyUnstable_Object_EnableDeferredRefcount;注意 PyUnstable_ 前缀接口的稳定性契约。
  5. C 类型的属性语义:不覆写 tp_getattro/tp_setattro 时默认落到 PyObject_GenericGetAttr/PyObject_GenericSetAttr 语义(数据描述符 > 实例字典),自写描述符时应与这一优先级对齐。

十二、小结

CPython 的 Object Protocol 是 C 扩展与 Python 对象世界之间的通用协议层:Doc/c-api/object.rst 定义的每个函数都把一条 Python 表达式语义(o.attrdel o[key]repr(o)format(o, spec)……)固化为具有精确引用计数与异常契约的 C 入口;Objects/object.cObjects/abstract.c 的实现进一步揭示了 CPython 在其中的工程权衡——GetOptionalAttr 的内置类型快速路径、HasAttr 的 unraisable 降级、SetAttr 的异常置位防护、PyObject_Dump 的释放内存启发式,都是"协议语义 + 性能/健壮性"两全的产物。理解这层协议及其版本演进,是编写正确、高效 C 扩展的基础。

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

项目优选

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