CPython C API 序列协议(Sequence Protocol)完全指南:PySequence_* 系列 API、槽位分发与实现原理
本文基于 CPython 官方 C API 文档 Sequence Protocol,系统讲解 PySequence_* 系列函数的语义、返回值约定与等价 Python 表达式,并结合 Objects/abstract.c 与 Include/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 sequence 的 TypeError;两者都没有则抛出 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):
- 优先调用序列槽位
sq_concat/sq_repeat; - 若槽位为空、且操作数通过
PySequence_Check,则回退到数字协议的nb_add/nb_multiply——因为用户类只定义__add__()/__mul__()时只有nb_add槽,没有sq_concat槽; - 都不可用则抛出
'%.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_concat→sq_concat→nb_inplace_add/nb_add;sq_inplace_repeat→sq_repeat→nb_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 表示失败(并置异常)。三条重要约定:
PySequence_SetItem不"偷取"(steal)v的引用,调用者负责维护自己的引用计数;- 文档明确:传
v == NULL表示删除元素,该用法已被废弃,应改用PySequence_DelItem; - 负索引同样会被自动归一化。
源码印证:PySequence_DelItem(Objects/abstract.c#L1925-L1956)内部就是调用 m->sq_ass_item(s, i, (PyObject *)NULL),即通过同一 __setitem__ 槽位传入 NULL 触发 __delitem__ 语义;SetItem 与 DelItem 对负索引的归一化代码完全一致。切片版本(Objects/abstract.c#L1958-L2002)则走 mp_ass_subscript,DelSlice 等价于 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_item 且 i 已是非负、界内索引。
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:返回value在o中出现的次数,失败返回-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_In是PySequence_Contains的别名,且已被 soft-deprecated(3.14):文档注明"新代码不应再使用",头文件中以#define PySequence_In PySequence_Contains兼容旧代码(Include/abstract.h#L762-L770;实现见 Objects/abstract.c#L2252-L2258,标注 "Backwards compatibility")。
这三个函数共享同一个底层引擎 _PySequence_IterSearch(Objects/abstract.c#L2131-L2227),一次迭代完成三种操作(COUNT / INDEX / CONTAINS)。值得注意的边界处理:
- 计数或索引达到
PY_SSIZE_T_MAX时抛出OverflowError(count 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_List(Objects/abstract.c#L2080-L2101):PyList_New(0)后通过_PyList_Extend一次性扩展,返回对象保证是新对象。PySequence_Tuple(Objects/abstract.c#L2004-L2078):文档承诺"若o本身是 tuple,返回其新引用;否则构造等内容的 tuple"。源码中有三条路径:- 精确 tuple(
PyTuple_CheckExact)直接Py_NewRef返回——注意源码注释特意说明:tuple 的子类不享受该快路径,因为无法安全地把子实例原样返回; - 精确 list 走
PyList_AsTuple; - 其他可迭代对象先用 8 元素栈上缓冲
buffer[8]承接前 8 项,超过后再落盘到 16 容量的PyListObject并逐项append,最后_PyList_AsTupleAndClear一次性转 tuple。这是为小序列避免堆分配的典型优化。
- 精确 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);
文档解释其命名由来:这些函数假设结果是 PyTupleObject 或 PyListObject,从而直接访问底层数据字段而免去方法查找开销。语义要点:
-
PySequence_Fast(o, m):把任意序列/可迭代对象o转为"Fast 家族可用"的对象;若o既不是序列也不可迭代,抛出以m为消息文本的TypeError(m通常写明参数期望,如"expected an iterable")。从源码结构看(Objects/abstract.c#L2103-L2129):精确 list/tuple 直接Py_NewRef返回(这正是文档所说"CPython 实现细节:若o已是序列或 list 则原样返回");否则PyObject_GetIter后统一转成 list——因此该函数的返回值在 CPython 中只会是精确的 list 或 tuple。 -
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)) -
PySequence_Fast_GET_ITEM(o, i):同样假设o来自PySequence_Fast、非NULL且i在界内(Include/cpython/abstract.h#L96-L97),内部即PyList_GET_ITEM/PyTuple_GET_ITEM。 -
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. 使用时的关键约定
- 引用计数:返回
PyObject*的函数(Concat/Repeat/GetItem/GetSlice/List/Tuple/Fast 等)成功时均返回新引用,调用者必须Py_DECREF;in-place 函数在原地修改时返回的是同一对象的新引用,同样要释放。 - 错误约定:返回
NULL/-1的函数失败时会设置 Python 异常,C 层应及时返回并交由解释器处理;PySequence_Check是唯一"永不失败"的函数。 - 负索引:
GetItem/SetItem/DelItem会自动归一化负索引(前提类型提供sq_length);而PySequence_ITEM宏与PySequence_Fast_GET_ITEM宏不做任何边界处理,索引合法性由调用者保证。 - 序列 vs 映射的边界:只实现了
__getitem__的自定义类通常也"算序列"(sq_item非空),但 dict 及其子类被PySequence_Check显式排除;切片读取/赋值实际走 mapping 的mp_subscript/mp_ass_subscript槽位,因此对象需同时支持切片下标才能使用GetSlice/SetSlice/DelSlice。
9. 参考资料(仓库内路径)
- 本文主体文档:Doc/c-api/sequence.rst
- 函数实现:Objects/abstract.c(序列操作区段约 L1678 起,另见 Objects/abstract.c#L2138 的
_PySequence_IterSearch) - 公开原型与别名宏:Include/abstract.h
PySequence_ITEM/PySequence_Fast_*宏定义:Include/cpython/abstract.h- 相邻协议可对照阅读:Doc/c-api/mapping.rst、Doc/c-api/iter.rst、Doc/c-api/tuple.rst、Doc/c-api/list.rst
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 StartedRust0624
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