首页
/ CPython 抽象对象层(Abstract Objects Layer)详解:类型无关的 C API 设计与源码级剖析

CPython 抽象对象层(Abstract Objects Layer)详解:类型无关的 C API 设计与源码级剖析

2026-09-06 11:32:47作者:董灵辛Dennis

CPython C 扩展开发中最常用的一类 API 是“抽象对象层”函数:它们不关心传入对象的具体类型,而是面向 listdictint、自定义对象等“宽类”统一操作,例如等价于 Python 表达式的 getattr+initer。本文以官方文档 Abstract Objects Layer 为骨架,完整梳理其七个子协议(对象、调用、数值、序列、映射、迭代器、缓冲区)的 API 语义,并结合 Objects/abstract.cInclude/abstract.h 的实现源码,深入讲解二目运算分派、NotImplemented 协议、下标查找回退链和迭代器耗尽判定等底层机制,帮助你在编写 C 扩展时既能正确调用这些函数,也能理解其错误约定与性能快路径。

一、章节定位:什么是抽象对象层

Doc/c-api/abstract.rst 开篇给出了三条关键约定,也是理解整个章节的前提:

  1. 类型无关性:本章节的函数作用于“任意 Python 对象”,或作用于某一类宽泛的对象集合(例如所有数值类型、所有序列类型)。
  2. 失败即抛异常:当函数被用于不适用的类型上时,会抛出 Python 异常(如 TypeError),而不是返回一个 C 层面的错误码。
  3. 初始化完整性要求:不能把这些函数用在“未正确初始化”的对象上——文档举例:一个通过 PyList_New 创建但元素尚未设置为非 NULL 的 list 对象,此时调用抽象层函数是未定义行为。

这个章节在文档树中由一个 toctree 组织成七个子章节,各自对应 Include/abstract.h 中的一个协议区块:

子协议 文档 头文件区块(源码注释)
对象协议 Object Protocol object.rst /* === Object Protocol === */
调用协议 Call Protocol call.rst PyObject_Call* 系列声明
数值协议 Number Protocol number.rst /* === Number Protocol === */
序列协议 Sequence Protocol sequence.rst /* === Sequence protocol === */
映射协议 Mapping Protocol mapping.rst /* === Mapping protocol === */
迭代器协议 Iterator Protocol iter.rst /* ==== Iterators ==== */
缓冲区协议 Buffer Protocol buffer.rst Include/pybuffer.h 单独提供

从源码结构看,前六个协议的公共实现集中在 Objects/abstract.c(约 2900 行),而对象协议中一部分最底层的函数(PyObject_RichComparePyObject_HashPyObject_IsTrue 等)实现在 Objects/object.c 中。

二、对象协议(Object Protocol):跨类型操作的基石

对象协议是抽象层中覆盖面最广的部分,API 定义见 Doc/c-api/object.rst。以下按功能分组梳理。

2.1 常量获取:Py_GetConstantPy_GetConstantBorrowed

Python 3.13 引入 Py_GetConstant(unsigned int constant_id),用于获取常量的强引用。文档中的常量表如下(数值 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 ()

文档明确:CPython 中这些常量全部是 immortal(永生对象),因此 Py_GetConstant 每次返回的都是同一强引用。3.13 同时提供 Py_GetConstantBorrowed 返回借用引用,但官方标注它主要为向后兼容保留,新代码推荐使用 Py_GetConstant

2.2 属性协议:HasAttr / GetAttr / SetAttr / DelAttr 家族

属性访问是 C 扩展与 Python 对象交互最高频的操作。文档给出了四组变体,语义差异需要精确掌握:

  • PyObject_GetAttr(o, attr_name):等价于 o.attr_name,成功返回新强引用,失败返回 NULL 并置异常。
  • PyObject_GetOptionalAttr(obj, attr_name, &result)(3.13 新增):属性不存在时不抛 AttributeError——返回 1 并置 *result 为新强引用;返回 0*resultNULLAttributeError 被静默);返回 -1 表示其他错误。
  • PyObject_SetAttr / PyObject_SetAttrString:等价于 o.attr_name = v。注意文档在 3.15 中的变更说明:不得在异常已设置时传入 NULLv——此时属性会被删除,这种组合通常源于遗漏了 NULL 检查。vNULL 表示删除属性,但该行为已弃用,官方建议改用 PyObject_DelAttr
  • PyObject_HasAttrWithError / PyObject_HasAttrStringWithError(3.13 新增):等价于 hasattr(o, name)会传播异常(返回 -1)。相比之下,PyObject_HasAttr 永远成功,其内部调用 __getattr__/__getattribute__ 产生的异常不会传播,而是交给 sys.unraisablehook——文档明确要求需要正确错误处理时改用 WithError 版本或 PyObject_GetOptionalAttr
  • PyObject_DelAttr / PyObject_DelAttrString:等价于 del o.attr

一个值得注意的实现细节在 Include/abstract.h:在 limited C API 3.12 及更早版本中,PyObject_DelAttr/PyObject_DelAttrString 被宏定义为 PyObject_SetAttr(o, name, NULL),3.13 起才成为独立函数。此外 PyObject_SetAttrString 的文档还给出了性能建议:属性名应通过静态字符串控制数量;编译期未知的名字应先用 PyUnicode_FromString 构造后调用 PyObject_SetAttr

通用属性槽:PyObject_GenericGetAttr / PyObject_GenericSetAttr 是可直接放进 PyTypeObjecttp_getattro/tp_setattro 槽的通用实现。从 Doc/c-api/object.rst 的语义描述看,getter 会在 MRO 各类型字典中查找描述符并在对象 __dict__ 中查找属性,数据描述符优先于实例属性;setter 则只查找数据描述符,优先于实例字典写入。PyObject_GenericGetDict/PyObject_GenericSetDict(3.3)分别提供 __dict__ 描述符的通用 getter/setter,其中 setter 不允许删除字典。

2.3 表示、比较与真值:Repr/Str/Hash/IsTrue

  • PyObject_Repr / PyObject_Str / PyObject_ASCII:分别等价于 repr(o)str(o)ascii(o);传 NULL 时返回 '<NULL>'PyObject_Str 同时是 print 的底层调用。
  • PyObject_Bytes:等价于 bytes(o),但有一个差异——当 o 是整数时 bytes(o) 会返回零初始化 bytes 对象,而 PyObject_Bytes 会抛 TypeError
  • PyObject_RichCompare(o1, o2, opid)opidPy_LT/Py_LE/Py_EQ/Py_NE/Py_GT/Py_GE,实现位于 Objects/object.cPyObject_RichCompareBool 额外返回 -1/0/1 三值,且有身份快捷路径:o1o2 是同一对象时,Py_EQ 恒返回 1Py_NE 恒返回 0
  • PyObject_Hash:等价于 hash(o),返回类型自 3.2 起为 Py_hash_t(与 Py_ssize_t 同宽的有符号整数),失败返回 -1;实现见 Objects/object.cPyObject_HashNotImplemented 可存入 tp_hash 槽,让类型显式声明自己不可哈希。
  • PyObject_IsTrue / PyObject_Not:分别等价于 not not onot o,实现见 Objects/object.c

2.4 类型检查与下标、目录

  • PyObject_Type:返回 type(o) 的新强引用;文档指出,若只需要类型指针而不需要引用,应使用 Py_TYPE() 宏。
  • PyObject_TypeCheck(o, type):判断 o 是否为 type 的实例或子类实例,两个参数都不得为 NULL
  • PyObject_IsSubclass / PyObject_IsInstance:语义与 issubclass/isinstance 完全一致——cls 为元组时逐项检查;cls 定义 __subclasscheck__/__instancecheck__ 时走 PEP 3119 协议;实例可通过 __class__ 属性、类可通过 __bases__ 属性覆盖默认判断。
  • PyObject_Size / PyObject_Length:等价于 len(o)。当对象同时提供序列与映射协议时返回序列长度PyObject_Length 只是 DLL 兼容别名)。
  • PyObject_LengthHint(o, default)(3.4):等价于 operator.length_hint,实现(Objects/abstract.c)的顺序是 __len____length_hint__ → 默认值,且会对 __length_hint__ 的结果做“必须返回非负整数”的校验。
  • 下标:PyObject_GetItem / PyObject_SetItem(不偷取 v 的引用)/ PyObject_DelItem / PyObject_DelItemString,分别对应 o[key]o[key] = vdel o[key]
  • PyObject_Dir(o):等价于 dir(o);传 NULL 时等价于无参 dir(),若无活动帧则返回 NULL 但不置异常。

2.5 3.12–3.15 新增的对象层 API

近年版本在对象协议中加入了多个面向类型系统与内存管理的新函数:

  • PyObject_GetTypeData / PyType_GetTypeDataSize / PyObject_GetItemData(3.12):分别用于访问负 basicsize 类型保留的子类数据区、Py_TPFLAGS_ITEMS_AT_END 类型的每元素数据区,文档反复强调“Python 不检查前置条件”,调用方必须自行保证类型确实以负 basicsize 方式创建。
  • PyObject_VisitManagedDict / PyObject_ClearManagedDict(3.13):仅在类型设置了 Py_TPFLAGS_MANAGED_DICT 时可在 traverse/clear 函数中调用。
  • PyUnstable_Object_EnableDeferredRefcount(3.14):在无 GIL 构建中为对象启用延迟引用计数——对象只由追踪式 GC 回收,从而避免多线程下的引用计数竞争;在 GIL 构建中为空操作。
  • PyUnstable_TryIncRef / PyUnstable_EnableTryIncRef(3.14):原子地“若引用计数非零则加一”,官方给出了用它实现类 WeakValueDictionary 的 weakmap 的完整 C 示例(Doc/c-api/object.rst)。
  • PyUnstable_Object_IsUniquelyReferenced(3.14):由于 3.14 起解释器在将对象加载到操作数栈时可能借用引用,Py_REFCNT == 1 不再能保证唯一引用,该函数在 free-threaded 构建下还会检查对象是否仅被当前线程使用。
  • PyUnstable_SetImmortal(3.15):将对象标记为 immortal,单向不可逆;immortal 对象不参与引用计数、永不被 GC 回收,若被 GC 追踪则会被解除追踪。
  • PyObject_Dump(op)(3.15):调试用对象转储,输出到 stderr,设计上要在内存损坏后仍尽量可调用(按“最不可能崩溃的字段优先”顺序输出),并带有“对象内存是否已释放”的启发式检测。

三、调用协议(Call Protocol)

Include/abstract.h 集中声明了调用家族的 C API,与 Doc/c-api/call.rst 对应。从声明顺序可以归纳出一套递进关系:

PyObject *PyObject_CallNoArgs(PyObject *func);                 /* 3.9+,无参调用 */
PyObject *PyObject_Call(PyObject *callable,
                        PyObject *args, PyObject *kwargs);     /* callable(*args, **kwargs) */
PyObject *PyObject_CallObject(PyObject *callable, PyObject *args);
PyObject *PyObject_CallFunction(PyObject *callable,
                                const char *format, ...);      /* mkvalue 格式串 */
PyObject *PyObject_CallMethod(PyObject *obj, const char *name,
                              const char *format, ...);
PyObject *PyObject_CallFunctionObjArgs(PyObject *callable, ...);  /* NULL 结尾的 PyObject* 列表 */
PyObject *PyObject_CallMethodObjArgs(PyObject *obj, PyObject *name, ...);
PyObject *PyObject_Vectorcall(PyObject *callable,
                              PyObject *const *args, size_t nargsf,
                              PyObject *kwnames);               /* PEP 590 */
PyObject *PyObject_VectorcallMethod(PyObject *name, PyObject *const *args,
                                     size_t nargsf, PyObject *kwnames);

版本门槛在头文件中以 Py_LIMITED_API 条件编译体现:PyObject_CallNoArgs 需要 3.9+(abstract.h),PyObject_Vectorcall/PyObject_VectorcallMethod 需要 3.12+ 且依赖 PY_VECTORCALL_ARGUMENTS_OFFSET 高位标记位(abstract.h)。PEP 590 风格的 vectorcall 直接接收裸 PyObject*const* 数组与关键字名元组,省去了构造 args 元组的开销,是 C 扩展中最高效的调用入口。

四、数值协议(Number Protocol):二目运算的分派机制

数值协议(Doc/c-api/number.rstInclude/abstract.h)暴露了完整的二元/一元/原地运算函数:PyNumber_AddPyNumber_OrPyNumber_InPlace* 系列、PyNumber_Power(三目)、以及转换函数 PyNumber_Index/PyNumber_AsSsize_t/PyNumber_Long/PyNumber_Float/PyNumber_ToBase

4.1 binary_op1:NotImplemented 驱动的槽位分派

所有二目运算共用 Objects/abstract.c 中的 binary_op1,源码注释写明分派顺序:

依次尝试:w.op(v,w)[仅当 w 是 v 的真子类时优先] → v.op(v,w) → w.op(v,w)

核心逻辑是:

  1. Py_TYPE(v)->tp_as_number 取 v 类型的 nb_* 槽位(宏 NB_BINOPoffsetof 从结构体中取函数指针);
  2. 仅当 w 与 v 不同类型时才取 w 类型的对应槽位,且若两槽位指向同一函数则置空以避免重复调用;
  3. 每个槽位调用后检查结果是否为 Py_NotImplemented——是则 Py_DECREF 并尝试下一个槽位(源码注释 /* can't do it */);
  4. 全部失败则返回 PyNotImplemented,由外层 binary_op 统一转换为 unsupported operand type(s) for <op>: '<t1>' and '<t2>'TypeErrorbinop_type_error)。

这正是 Python 层面 __add__ 返回 NotImplemented 以让反向操作数接管运算的 C 层实现,也是 Doc/c-api/object.rstPy_NotImplemented 单例与 Py_RETURN_NOTIMPLEMENTED 宏存在的意义。

4.2 数值协议与序列协议的交叉回退

两个特例展示了抽象层跨协议协作的设计:

  • PyNumber_Addnb_add 槽位全部返回 NotImplemented 后,会回退到左操作数的序列槽 sq_concat——所以 list + list 在 C 层最终走的是序列拼接而非数值加法。
  • PyNumber_Multiplynb_multiply 失败后回退到 sq_repeat,且左右两侧都尝试;sequence_repeat 辅助函数(abstract.c)会先把次数参数通过 PyNumber_AsSsize_t 转成 Py_ssize_t,非整数直接报 can't multiply sequence by non-int of type ...

三目运算 PyNumber_Power(v, w, z) 则走 ternary_op,分派顺序为 v.op(v,w,z) → w.op(v,w,z) → z.op(v,w,z);当 zNone 时错误消息只显示两个操作数类型。

4.3 原地运算的“无槽即回退”约定

Objects/abstract.c 的注释明确约定:原地运算符(+=*= 等)若左操作数没有对应的 nb_inplace_* 槽,则按普通运算处理——返回结果可以是原对象,也可以是新对象。binary_iop1 的实现正是“先试 inplace 槽,失败(NotImplemented)则降级为普通 binary_op1”。

4.4 PyNumber_IndexPyNumber_AsSsize_t

PyIndex_Check 只检查类型是否填充了 nb_index 槽;PyNumber_Index 将其转换为 Python intPyNumber_AsSsize_t(o, exc) 在此基础上进一步转成 C 的 Py_ssize_t,转换溢出时若 excNULL 则抛出该异常,为 NULL 则清错并钳位。这是 C 扩展把 Python 侧索引/长度参数安全转成 C 整数的标准途径,序列与映射协议的下标函数内部均依赖它。

五、序列协议与映射协议

5.1 序列协议

API 与 Doc/c-api/sequence.rst 对应,包括 PySequence_CheckPySequence_SizePySequence_Concat/RepeatPySequence_GetItem/SetItem/DelItemPySequence_GetSlice/SetSlice/DelSlicePySequence_Tuple/ListPySequence_CountPySequence_Contains(源码级兼容别名 PySequence_In)、PySequence_Index,以及原地版本 PySequence_InPlaceConcat/InPlaceRepeat(“尽可能原地,返回结果可能是 o1 本身”)。

PySequence_Fast(o, m) 是 C 扩展热路径上最重要的函数:它把对象统一转成 list/tuple(已是 tuple/list 则原样返回),配合 PySequence_Fast_GET_ITEM/PySequence_Fast_GET_SIZE 宏实现 O(1) 元素访问;不支持迭代的对象会以 m 作为消息文本抛出 TypeError

5.2 映射协议

映射协议(Doc/c-api/mapping.rst)提供 PyMapping_CheckPyMapping_SizePyMapping_HasKey(String)(永远成功,吞掉异常)与 3.13 新增的 PyMapping_HasKeyWithError(String)(失败返回 -1)、PyMapping_Keys/Values/ItemsPyMapping_GetItemStringPyMapping_SetItemString,以及 3.13 的 PyMapping_GetOptionalItem(String)(不抛 KeyError 的三值变体,limited API 门槛 3.13,见 abstract.h)。PyMapping_DelItem(String) 在头文件中直接是 PyObject_DelItem(String) 的宏别名。

5.3 实现中的协议优先级:序列优先于映射

Objects/abstract.cPyObject_Size 看,长度查找顺序是:先序列槽 sq_length,再映射PyMapping_Size)——这就是文档“两者皆有则返回序列长度”约定的落点。

而下标查找 PyObject_GetItem 恰好相反,回退链为:

  1. tp_as_mapping->mp_subscript(即 __getitem__ 语义);
  2. tp_as_sequence->sq_item:仅当 key 是索引对象(_PyIndex_Check)时,经 PyNumber_AsSsize_t 转成 Py_ssize_t 再走 PySequence_GetItem,否则报 sequence index must be integer
  3. 特例:对 type 对象,int[...]Py_GenericAlias,其他类走 __class_getitem__,没有则报 type 'X' is not subscriptable
  4. 其余对象报 'X' object is not subscriptable

性能上还有一个 dict 快路径:PyMapping_GetOptionalItem 对精确的 dict 类型直接调用 PyDict_GetItemRef,绕过通用 PyObject_GetItem 的槽位调用与异常清理。

六、迭代器协议(Iterator Protocol)

Doc/c-api/iter.rst 定义了迭代器协议的全部函数:PyIter_CheckPyAIter_Check(3.10)、PyIter_NextItem(3.14)、PyIter_NextPySendResult 枚举与 PyIter_Send(3.10)。

6.1 获取迭代器:PyObject_GetIter 的回退链

Objects/abstract.c 实现了清晰的三级策略:

  1. 类型填充了 tp_iter 槽则直接调用,并校验返回值确实是迭代器(否则报 X.__iter__() must return an iterator, not Y);
  2. tp_iter 但通过 PySequence_Check(即有 __getitem__ 序列语义)则构造 PySeqIter_New 序列迭代器——这就是老式 __getitem__ 迭代协议的 C 层实现;
  3. 都不是则报 'X' object is not iterable

异步版本 PyObject_GetAIterabstract.c)只检查 tp_as_async->am_aiter,无序列回退。

6.2 “耗尽”与“出错”的判定:iternext 静态函数

这是迭代器协议最精妙的部分。iternext 的判定规则:

  • tp_iternext 返回非 NULL → 正常取到值,返回 1
  • 返回 NULL无异常 → 正常耗尽,返回 0
  • 返回 NULL 且异常是 StopIteration清掉异常,视为耗尽,返回 0(源码注释:a StopIteration exception may or may not be set,即容忍实现里“忘了清 StopIteration”的迭代器);
  • 返回 NULL 且异常是其他类型 → 错误,返回 -1

3.14 新增的 PyIter_NextItem 将这一三值语义公开为稳定 API(1 = 取到值、0 = 耗尽、-1 = 出错),文档明确要求新代码优先使用它替代 PyIter_NextPyIter_Next 仅以“耗尽/出错都返回 NULL”的方式暴露,调用方必须自行用 PyErr_Occurred() 区分。PyIter_Checkabstract.c)则以 tp_iternext 非空且不等于哨兵 _PyObject_NextNotImplemented 为判据。

6.3 向迭代器/生成器发值:PyIter_Send

PyIter_Send 返回 PYGEN_NEXT(yield 出值)/PYGEN_RETURN(生成器返回,值经 StopIteration 提取到 presult)/PYGEN_ERROR。实现上有三条路径:类型提供 am_send 槽则直接调用;argNone 且对象是迭代器则直接走 tp_iternext;否则调用 send(arg) 方法。这为 C 扩展中的 async for 与生成器驱动提供了与解释器一致的行为。

七、缓冲区协议与整体使用约定

缓冲区协议(buffer.rst)声明位于 Include/pybuffer.h,提供 PyObject_GetBuffer/PyBuffer_FillInfo/PyBuffer_IsContiguous 等函数,让 C 代码以 Py_buffer 结构零拷贝访问支持 buffer 协议的对象(如 bytesarraynumpy 数组)。它与其余六个协议的关系是:抽象对象层负责“是什么/有什么操作”,缓冲区协议负责“原始内存如何暴露”。

综合全章节,使用抽象层 API 需遵守的三条工程约定(均见 Doc/c-api/abstract.rst 与头文件声明):

  1. 错误约定:对象/调用/数值类函数失败时返回 NULL 且异常已置位;int 返回值函数失败返回 -1;“永远成功”的函数(PyCallable_CheckPyIter_CheckPyMapping_HasKeyPySequence_Check 等)在头文件注释中逐一标明。
  2. 引用计数:返回 PyObject* 的查询类函数一律返回新强引用,由调用方负责 Py_DECREFPyObject_SetItem 等写入函数不偷取引用。
  3. 版本门槛:头文件用 Py_LIMITED_API 条件编译控制符号可见性(例如 abstract.hPyObject_DelAttrString 在 limited 3.12 及之前只是宏、abstract.hPyIter_NextItem 要求 3.14+),跨版本扩展应按这些门槛选择 API。

八、验证材料与延伸阅读

按这套“文档语义 + 源码分派链”的阅读方式,你可以对任何一个抽象层函数快速回答三个问题:它对哪些类型生效、失败时抛什么异常、以及引用该由谁释放——这正是编写健壮 C 扩展所需的全部判断依据。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 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
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388