首页
/ CPython C API 序列协议(Sequence Protocol)完全指南:PySequence_* 系列 API、槽位分发与实现原理

CPython C API 序列协议(Sequence Protocol)完全指南:PySequence_* 系列 API、槽位分发与实现原理

2026-09-06 13:00:38作者:凌朦慧Richard

本文基于 CPython 官方 C API 文档 Sequence Protocol,系统讲解 PySequence_* 系列函数的语义、返回值约定与等价 Python 表达式,并结合 Objects/abstract.cInclude/abstract.h 的源码,剖析每个函数背后的槽位(slot)分发逻辑与回退链路。读完本文,你可以在 C 扩展中安全地实现"把任意 Python 可迭代对象当序列处理"的通用逻辑(如 list(o)o[i]value in o),并理解 CPython 为何在 PySequence_Fast 家族上做了特殊的性能优化。

1. 什么是序列协议

在 CPython 的类型系统中,每个 PyTypeObject 通过 tp_as_sequence 字段挂接一个 PySequenceMethods 结构体,其中各槽位对应 Python 序列行为:

槽位 对应 Python 行为
sq_length len(o)
sq_concat o1 + o2
sq_repeat o * n
sq_inplace_concat o1 += o2
sq_inplace_repeat o *= n
sq_item o[i]
sq_ass_item o[i] = v / del o[i]
sq_contains v in o

PySequence_* 系列 C API 的作用,就是让扩展模块无需直接调用对象的方法,就能在 C 层完成"序列化"操作。文档 Doc/c-api/sequence.rst 共定义了 20 余个公开函数,下面按用途分组逐一讲解,并标注其在 Objects/abstract.c 中的实现位置。

2. 类型检查与长度:PySequence_Check / PySequence_Size

PySequence_Check

int PySequence_Check(PyObject *o);

若对象提供序列协议则返回 1,否则返回 0。文档特别指出:定义了 __getitem__ 方法的 Python 类一般也会返回 1(因为 __getitem__ 会映射到 sq_item 槽位),但 dict 的实例与子类除外——无法确定 dict 子类的键类型,故一律拒绝。该函数永不失败。

源码印证(Objects/abstract.c#L1680-L1687):

int
PySequence_Check(PyObject *s)
{
    if (PyDict_Check(s))
        return 0;
    return Py_TYPE(s)->tp_as_sequence &&
        Py_TYPE(s)->tp_as_sequence->sq_item != NULL;
}

判断依据正是"非 dict 且 sq_item 槽位非空"。

PySequence_Size 与 PySequence_Length

Py_ssize_t PySequence_Size(PyObject *o);
Py_ssize_t PySequence_Length(PyObject *o);

成功返回序列 o 中元素个数,失败返回 -1。等价于 Python 的 len(o)。在 Include/abstract.h#L680-L682 中两者通过 #define PySequence_Length PySequence_Size 是同一函数。

实现细节(Objects/abstract.c#L1689-L1710):优先调用 sq_length;若该对象没有 sq_length 但提供了 mapping 协议的 mp_length,会抛出形如 '%.200s' is not a sequenceTypeError;两者都没有则抛出 object of type '%.200s' has no len()

3. 自身运算:拼接、重复及其 in-place 版本

PySequence_Concat / PySequence_Repeat

PyObject *PySequence_Concat(PyObject *o1, PyObject *o2);   // o1 + o2
PyObject *PySequence_Repeat(PyObject *o, Py_ssize_t count); // o * count

成功返回新对象(新引用),失败返回 NULL

从源码结构看,这两个函数存在两级回退链路Objects/abstract.c#L1720-L1775):

  1. 优先调用序列槽位 sq_concat / sq_repeat
  2. 若槽位为空、且操作数通过 PySequence_Check,则回退到数字协议nb_add / nb_multiply——因为用户类只定义 __add__() / __mul__() 时只有 nb_add 槽,没有 sq_concat 槽;
  3. 都不可用则抛出 '%.200s' object can't be concatenated(或 can't be repeated)类型的 TypeError

PySequence_InPlaceConcat / PySequence_InPlaceRepeat

PyObject *PySequence_InPlaceConcat(PyObject *o1, PyObject *o2); // o1 += o2
PyObject *PySequence_InPlaceRepeat(PyObject *o, Py_ssize_t count); // o *= count

等价于 += / *=,当 o1 / o 支持 in-place 操作时原地执行。实现上的回退顺序为(Objects/abstract.c#L1777-L1838):

  • sq_inplace_concatsq_concatnb_inplace_add / nb_add
  • sq_inplace_repeatsq_repeatnb_inplace_multiply / nb_multiply

即 in-place 版本在类型不支持原地修改时,自动退化为普通版本的结果(仍返回新对象)。

4. 元素与切片的访问、赋值、删除

读取:PySequence_GetItem / PySequence_GetSlice

PyObject *PySequence_GetItem(PyObject *o, Py_ssize_t i);      // o[i]
PyObject *PySequence_GetSlice(PyObject *o, Py_ssize_t i1, Py_ssize_t i2); // o[i1:i2]
  • PySequence_GetItem 实现于 Objects/abstract.c#L1840-L1868。关键点:它会自动处理负索引——当 i < 0 且类型提供 sq_length 时,先求长度再把 i 调整为 i + len(o),然后调用 sq_item。因此 C 层可直接传 -1 表示"最后一个元素"。若对象只有 mapping 协议的 mp_subscript,会抛出 '%.200s' is not a sequence
  • PySequence_GetSlice 实现于 Objects/abstract.c#L1870-L1890,走的是 mp_subscript 路径:内部先用 _PySlice_FromIndices(i1, i2) 构造 slice 对象,再以该 slice 作为下标调用 __getitem__,失败类型抛 '%.200s' object is unsliceable

写入与删除:SetItem / DelItem / SetSlice / DelSlice

int PySequence_SetItem(PyObject *o, Py_ssize_t i, PyObject *v);  // o[i] = v
int PySequence_DelItem(PyObject *o, Py_ssize_t i);              // del o[i]
int PySequence_SetSlice(PyObject *o, Py_ssize_t i1, Py_ssize_t i2, PyObject *v); // o[i1:i2] = v
int PySequence_DelSlice(PyObject *o, Py_ssize_t i1, Py_ssize_t i2);               // del o[i1:i2]

统一返回 0 表示成功、-1 表示失败(并置异常)。三条重要约定:

  1. PySequence_SetItem 不"偷取"(steal)v 的引用,调用者负责维护自己的引用计数;
  2. 文档明确:v == NULL 表示删除元素,该用法已被废弃,应改用 PySequence_DelItem
  3. 负索引同样会被自动归一化。

源码印证:PySequence_DelItemObjects/abstract.c#L1925-L1956)内部就是调用 m->sq_ass_item(s, i, (PyObject *)NULL),即通过同一 __setitem__ 槽位传入 NULL 触发 __delitem__ 语义;SetItemDelItem 对负索引的归一化代码完全一致。切片版本(Objects/abstract.c#L1958-L2002)则走 mp_ass_subscriptDelSlice 等价于 mp_ass_subscript(s, slice, NULL)

快速路径:PySequence_ITEM 宏

PyObject* PySequence_ITEM(PyObject *o, Py_ssize_t i);

文档将其描述为 PySequence_GetItem 的"更快版本":不检查 PySequence_Check(o) 是否为真,也不对负索引做调整。其本质是一个宏,直接解引用槽位(Include/cpython/abstract.h#L84-L87):

#define PySequence_ITEM(o, i)\
    ( Py_TYPE(o)->tp_as_sequence->sq_item((o), (i)) )

使用时必须自行保证 o 具备 sq_itemi 已是非负、界内索引。

5. 搜索与成员测试:Count / Contains / Index

Py_ssize_t PySequence_Count(PyObject *o, PyObject *value);   // o.count(value)
int PySequence_Contains(PyObject *o, PyObject *value);       // value in o
Py_ssize_t PySequence_Index(PyObject *o, PyObject *value);   // o.index(value)
  • PySequence_Count:返回 valueo 中出现的次数,失败返回 -1
  • PySequence_Contains:有任一元素等于 value 返回 1,否则 0,出错返回 -1。实现优先使用 sq_contains__contains__),否则回退到迭代查找(Objects/abstract.c#L2239-L2250,头文件注释见 Include/abstract.h#L758:"Use __contains__ if possible, else _PySequence_IterSearch()")。
  • PySequence_Index:返回首个满足 o[i] == value 的下标,出错返回 -1
  • PySequence_InPySequence_Contains 的别名,且已被 soft-deprecated(3.14):文档注明"新代码不应再使用",头文件中以 #define PySequence_In PySequence_Contains 兼容旧代码(Include/abstract.h#L762-L770;实现见 Objects/abstract.c#L2252-L2258,标注 "Backwards compatibility")。

这三个函数共享同一个底层引擎 _PySequence_IterSearchObjects/abstract.c#L2131-L2227),一次迭代完成三种操作(COUNT / INDEX / CONTAINS)。值得注意的边界处理:

  • 计数或索引达到 PY_SSIZE_T_MAX 时抛出 OverflowErrorcount exceeds C integer size / index exceeds C integer size);
  • Index 找不到元素时抛出 ValueError: sequence.index(x): x not in sequence
  • 比较使用 PyObject_RichCompareBool(item, obj, Py_EQ),即依赖元素的 == 语义。

6. 序列转换:List / Tuple / Fast 家族

PySequence_List 与 PySequence_Tuple

PyObject *PySequence_List(PyObject *o);   // list(o),保证返回全新 list
PyObject *PySequence_Tuple(PyObject *o);  // tuple(o)
  • PySequence_ListObjects/abstract.c#L2080-L2101):PyList_New(0) 后通过 _PyList_Extend 一次性扩展,返回对象保证是新对象
  • PySequence_TupleObjects/abstract.c#L2004-L2078):文档承诺"若 o 本身是 tuple,返回其新引用;否则构造等内容的 tuple"。源码中有三条路径:
    1. 精确 tuplePyTuple_CheckExact)直接 Py_NewRef 返回——注意源码注释特意说明:tuple 的子类不享受该快路径,因为无法安全地把子实例原样返回;
    2. 精确 listPyList_AsTuple
    3. 其他可迭代对象先用 8 元素栈上缓冲 buffer[8] 承接前 8 项,超过后再落盘到 16 容量的 PyListObject 并逐项 append,最后 _PyList_AsTupleAndClear 一次性转 tuple。这是为小序列避免堆分配的典型优化。

PySequence_Fast 家族:C 扩展最常用的序列入口

PyObject *PySequence_Fast(PyObject *o, const char *m);
Py_ssize_t PySequence_Fast_GET_SIZE(PyObject *o);
PyObject *PySequence_Fast_GET_ITEM(PyObject *o, Py_ssize_t i);
PyObject **PySequence_Fast_ITEMS(PyObject *o);

文档解释其命名由来:这些函数假设结果是 PyTupleObjectPyListObject,从而直接访问底层数据字段而免去方法查找开销。语义要点:

  1. PySequence_Fast(o, m):把任意序列/可迭代对象 o 转为"Fast 家族可用"的对象;若 o 既不是序列也不可迭代,抛出以 m 为消息文本的 TypeErrorm 通常写明参数期望,如 "expected an iterable")。从源码结构看(Objects/abstract.c#L2103-L2129):精确 list/tuple 直接 Py_NewRef 返回(这正是文档所说"CPython 实现细节:若 o 已是序列或 list 则原样返回");否则 PyObject_GetIter 后统一转成 list——因此该函数的返回值在 CPython 中只会是精确的 list 或 tuple。

  2. PySequence_Fast_GET_SIZE(o):假定 o 来自 PySequence_Fast 且非 NULL。比 PySequence_Size 快,因为可断定 o 是 list 或 tuple。宏定义(Include/cpython/abstract.h#L91-L92):

    #define PySequence_Fast_GET_SIZE(o) \
        (PyList_Check(o) ? PyList_GET_SIZE(o) : PyTuple_GET_SIZE(o))
    
  3. PySequence_Fast_GET_ITEM(o, i):同样假设 o 来自 PySequence_Fast、非 NULLi 在界内(Include/cpython/abstract.h#L96-L97),内部即 PyList_GET_ITEM / PyTuple_GET_ITEM

  4. PySequence_Fast_ITEMS(o):返回底层 PyObject** 数组指针,便于 for (i = 0; i < n; i++) items[i] 式循环(Include/cpython/abstract.h#L101-L103)。文档警告:list 扩容可能让 ob_item 数组被重定位,故只有在序列不可能被修改的上下文中才可持有该指针;tuple 则无此风险。

一个典型的 C 扩展用法(把"任意可迭代参数"规约后逐项访问):

static PyObject *
sum2(PyObject *Py_UNUSED(self), PyObject *seq)
{
    PyObject *fast = PySequence_Fast(seq, "expected an iterable");
    if (fast == NULL)
        return NULL;

    Py_ssize_t n = PySequence_Fast_GET_SIZE(fast);
    long total = 0;
    for (Py_ssize_t i = 0; i < n; i++) {
        PyObject *item = PySequence_Fast_GET_ITEM(fast, i);
        /* 按需转换/累加,此处仅示意访问方式 */
        (void)item;
    }

    Py_DECREF(fast);          /* 若 fast 是原 list/tuple,此为归还 Py_NewRef 的引用 */
    return PyLong_FromLong(total);
}

7. 完整 API 速查表

函数 等价 Python 成功返回 失败返回
PySequence_Check —(类型判断) 1 0(永不失败)
PySequence_Size / PySequence_Length len(o) 元素数 -1
PySequence_Concat o1 + o2 新对象 NULL
PySequence_Repeat o * count 新对象 NULL
PySequence_InPlaceConcat o1 += o2 拼接结果(支持时原地) NULL
PySequence_InPlaceRepeat o *= count 重复结果(支持时原地) NULL
PySequence_GetItem o[i] 第 i 项(新引用) NULL
PySequence_GetSlice o[i1:i2] 切片对象(新引用) NULL
PySequence_SetItem o[i] = v 0 -1(置异常;不偷取 v 引用;v 为 NULL 删除已废弃)
PySequence_DelItem del o[i] 0 -1
PySequence_SetSlice o[i1:i2] = v 0 -1
PySequence_DelSlice del o[i1:i2] 0 -1
PySequence_Count o.count(value) 出现次数 -1
PySequence_Contains value in o 1 / 0 -1
PySequence_In 同 Contains 同上 -1(3.14 起 soft-deprecated)
PySequence_Index o.index(value) 首个下标 -1
PySequence_List list(o) 全新 list NULL
PySequence_Tuple tuple(o) tuple(入参为 tuple 时为其新引用) NULL
PySequence_Fast 规约为 list/tuple list/tuple(可能为新引用) NULL(非可迭代时抛 TypeError(m))
PySequence_Fast_GET_SIZE 长度 无(宏,调用前提见正文)
PySequence_Fast_GET_ITEM 第 i 项 无(宏,调用前提见正文)
PySequence_Fast_ITEMS 底层元素数组指针 无(宏,列表扩容会使其失效)

所有原型声明见 Include/abstract.h#L674-L792

8. 使用时的关键约定

  1. 引用计数:返回 PyObject* 的函数(Concat/Repeat/GetItem/GetSlice/List/Tuple/Fast 等)成功时均返回新引用,调用者必须 Py_DECREF;in-place 函数在原地修改时返回的是同一对象的新引用,同样要释放。
  2. 错误约定:返回 NULL / -1 的函数失败时会设置 Python 异常,C 层应及时返回并交由解释器处理;PySequence_Check 是唯一"永不失败"的函数。
  3. 负索引GetItem/SetItem/DelItem 会自动归一化负索引(前提类型提供 sq_length);而 PySequence_ITEM 宏与 PySequence_Fast_GET_ITEM 宏不做任何边界处理,索引合法性由调用者保证。
  4. 序列 vs 映射的边界:只实现了 __getitem__ 的自定义类通常也"算序列"(sq_item 非空),但 dict 及其子类被 PySequence_Check 显式排除;切片读取/赋值实际走 mapping 的 mp_subscript / mp_ass_subscript 槽位,因此对象需同时支持切片下标才能使用 GetSlice/SetSlice/DelSlice

9. 参考资料(仓库内路径)

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