CPython C API Object Protocol 详解:PyObject_* 函数族完整参考与源码级实现剖析
CPython 的 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 新增的无 GIL(free-threaded)构建下的引用计数辅助接口。读完本文,你将能够准确选型正确的 Object Protocol 函数、理解其引用计数与异常语义,并在 C 扩展中安全地访问、修改和比较任意 Python 对象。
一、Object Protocol 总览
Object Protocol 声明在 Include/object.h,实现分散在两个核心文件:
- Objects/object.c:常量获取、打印与调试转储、属性访问主入口、
PyObject_Repr/PyObject_Str等; - Objects/abstract.c:
PyObject_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 dict) | PyObject_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 明确不 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) 将对象打印到文件流,失败返回 -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_HasAttrString
PyObject_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.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_HasAttrWithError 与 PyObject_HasAttrStringWithError 语义与 hasattr() 等价:有属性返回 1,无属性返回 0,真实异常时返回 -1(异常置位,由调用者处理)。实现上,PyObject_HasAttrWithError 复用 PyObject_GetOptionalAttr 后 Py_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);
...
}
即:对通用对象、类型对象、模块对象,直接调用内部实现并"抑制缺失异常",省去了先抛 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 起的新约束(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 槽:
- 沿 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_GetDictPtr
PyObject_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_RichCompareBool
PyObject_RichCompare(PyObject *o1, PyObject *o2, int opid) 执行 o1 op o2,opid 必须为 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 o,1真 /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):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_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()——返回当前局部变量名,若无活动帧则返回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_GetTypeData
CPython 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_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+)
返回 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_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_GetTypeData、PyType_GetTypeDataSize、PyObject_GetItemData 引入 |
| 3.13 | Py_GetConstant、Py_GetConstantBorrowed、PyObject_HasAttrWithError、PyObject_HasAttrStringWithError、PyObject_GetOptionalAttr、PyObject_GetOptionalAttrString、PyObject_VisitManagedDict、PyObject_ClearManagedDict 引入 |
| 3.14 | PyUnstable_Object_EnableDeferredRefcount、PyUnstable_Object_IsUniqueReferencedTemporary、PyUnstable_IsImmortal、PyUnstable_TryIncRef、PyUnstable_EnableTryIncRef、PyUnstable_Object_IsUniquelyReferenced 引入 |
| 3.15 | PyObject_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 扩展的基础。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00