CPython List Objects C API 深度解析:从 PyList_New 到 PyList_AsTuple 的引用语义与实现细节
本文围绕 CPython 官方 C API 文档中的 List Objects 一章,系统讲解 PyListObject 类型及其配套的 PyList_* 系列 C 函数:类型检查、创建、读写元素、切片操作、原地排序等。文中每个 API 都会结合 Objects/listobject.c 与 Include/listobject.h 的源码印证其行为细节、引用计数语义(尤其是"窃取引用"的陷阱)以及 free-threaded(无 GIL)构建下的线程安全注意事项,帮助 C 扩展开发者写出正确且高效的 list 操作代码。
类型系统:PyListObject 与 PyList_Type
列表对象是 CPython 中最常用的可变序列类型。C API 文档 Doc/c-api/list.rst 首先定义了两个核心符号:
PyListObject:表示 Python 列表对象的PyObject子类型;PyList_Type:PyTypeObject实例,代表 Python 层的list类型,与 Python 层内置的list是同一个对象。
从 Include/cpython/listobject.h 可以看到 PyListObject 的内存布局:
typedef struct {
PyObject_VAR_HEAD
/* Vector of pointers to list elements. list[0] is ob_item[0], etc. */
PyObject **ob_item;
/* ob_item contains space for 'allocated' elements. The number
* currently in use is ob_size.
* Invariants:
* 0 <= ob_size <= allocated
* len(list) == ob_size
* ob_item == NULL implies ob_size == allocated == 0
* list.sort() temporarily sets allocated to -1 to detect mutations.
*
* Items must normally not be NULL, except during construction when
* the list is not yet visible outside the function that builds it.
*/
Py_ssize_t allocated;
} PyListObject;
几个要点值得注意:
ob_item是一个PyObject *指针数组,list[i]对应ob_item[i];allocated记录实际分配的容量,ob_size(即PyObject_VAR_HEAD中的大小字段)记录逻辑长度,二者满足0 <= ob_size <= allocated;- 头部注释明确指出:元素槽位正常情况下不应为 NULL,唯一例外是构造期间——这正是
PyList_New注意事项的根源,见下文。 list.sort()会把allocated临时置为 -1 以检测排序期间的元素变更。
PyList_Check 与 PyList_CheckExact
| 函数 | 语义 |
|---|---|
int PyList_Check(PyObject *p) |
判断 p 是否为 list 对象或 list 类型的子类实例,总是成功 |
int PyList_CheckExact(PyObject *p) |
判断 p 是否为 list 对象,但不接受 list 子类实例,总是成功 |
Include/listobject.h 中两者都是宏,实现非常轻量:
#define PyList_Check(op) \
PyType_FastSubclass(Py_TYPE(op), Py_TPFLAGS_LIST_SUBCLASS)
#define PyList_CheckExact(op) Py_IS_TYPE((op), &PyList_Type)
PyList_Check 走的是"快速子类标志位"检查(一次位运算),而 PyList_CheckExact 直接比较类型对象指针。注意:与很多 C API 函数不同,大多数 PyList_* 函数并不接受 list 子类——例如 PyList_Size 在传入非 list 对象时会调用 PyErr_BadInternalCall(),从 Objects/listobject.c 可以确认这一点:
Py_ssize_t
PyList_Size(PyObject *op)
{
if (!PyList_Check(op)) {
PyErr_BadInternalCall();
return -1;
}
else {
return PyList_GET_SIZE(op);
}
}
因此如果你需要操作"任意实现了序列协议的"对象,应改用 abstract 中定义的抽象 API(如 PySequence_* 系列)。
创建与大小:PyList_New、PyList_Size 与 NULL 槽位陷阱
PyList_New(Py_ssize_t len)
PyObject* PyList_New(Py_ssize_t len);
成功时返回长度为 len 的新列表,失败返回 NULL。官方文档特别给出了一段重要警告:当 len > 0 时,返回列表的元素槽位被初始化为 NULL。在把所有槽位用 PyList_SetItem 或 PyList_SET_ITEM() 填充为真实对象之前,不能将该对象暴露给 Python 代码,也不能调用 PySequence_SetItem 等抽象 API。文档给出的"安全窗口"API 只有 PyList_SetItem() 和 PyList_SET_ITEM()。
对照 Objects/listobject.c 的实现可以看到槽位确实被清零:
PyObject *
PyList_New(Py_ssize_t size)
{
if (size < 0) {
PyErr_BadInternalCall();
return NULL;
}
PyListObject *op = _Py_FREELIST_POP(PyListObject, lists);
if (op == NULL) {
op = PyObject_GC_New(PyListObject, &PyList_Type);
...
}
if (size <= 0) {
op->ob_item = NULL;
}
else {
...
memset(&array->ob_item, 0, size * sizeof(PyObject *)); /* 槽位置 NULL */
op->ob_item = array->ob_item;
...
}
Py_SET_SIZE(op, size);
op->allocated = size;
_PyObject_GC_TRACK(op);
return (PyObject *) op;
}
此外还可以观察到两个实现细节:
- 对象 freelist:
PyList_New优先从进程内 freelist(_Py_FREELIST_POP)复用刚释放的PyListObject,减少 GC 跟踪对象的分配开销; - GC 注册:新列表立即
_PyObject_GC_TRACK,因为它持有指向其他对象的指针槽位,需要参与循环引用回收。
PyList_Size 与 PyList_GET_SIZE
Py_ssize_t PyList_Size(PyObject *list); /* 等价于 len(list) */
Py_ssize_t PyList_GET_SIZE(PyObject *list); /* 无类型检查的宏形式 */
PyList_GET_SIZE 是 Include/cpython/listobject.h 中的 static inline 函数。在 free-threaded 构建中它通过原子 relaxed 读取 ob_size,因为其他线程可能正在并发修改列表长度;GIL 构建下则直接读取:
static inline Py_ssize_t PyList_GET_SIZE(PyObject *op) {
PyListObject *list = _PyList_CAST(op);
#ifdef Py_GIL_DISABLED
return _Py_atomic_load_ssize_relaxed(&(_PyVarObject_CAST(list)->ob_size));
#else
return Py_SIZE(list);
#endif
}
读取元素:PyList_GetItemRef、PyList_GetItem 与 PyList_GET_ITEM
这是 List C API 中语义差异最大的一组函数,官方文档在 Python 3.13 做了明确的"推荐"调整。
PyList_GetItemRef(3.13 新增,推荐)
PyObject* PyList_GetItemRef(PyObject *list, Py_ssize_t index);
返回 list[index] 位置的对象的强引用(strong reference),调用者负责 Py_DECREF。index 必须非负,不支持从列表末尾索引;越界(< 0 或 >= len(list))时返回 NULL 并设置 IndexError。
Objects/listobject.c 中的实现:
PyObject *
PyList_GetItemRef(PyObject *op, Py_ssize_t i)
{
if (!PyList_Check(op)) {
PyErr_SetString(PyExc_TypeError, "expected a list");
return NULL;
}
PyObject *item = list_get_item_ref((PyListObject *)op, i);
if (item == NULL) {
PyErr_SetObject(PyExc_IndexError, &_Py_STR(list_err)); /* "list index out of range" */
return NULL;
}
return item;
}
在 free-threaded 构建下,内部快路径 list_get_item_ref(Objects/listobject.c)会先尝试无锁的原子 incref(_Py_TryXGetRef);一旦检测到列表可能已被共享或元素在 incref 前被并发改掉,则回退到持对象锁的慢路径 list_item_impl,使用 Py_BEGIN_CRITICAL_SECTION 包裹读取。这是 3.13 引入该 API 的核心动机:返回强引用后,即使另一线程立即删除或替换了该槽位,你的对象依然安全。
PyList_GetItem(借用引用)
PyObject* PyList_GetItem(PyObject *list, Py_ssize_t index);
行为与 PyList_GetItemRef 相同,但返回的是借用引用(borrowed reference)——不增加引用计数,Objects/listobject.c 中直接返回 ob_item[i] 指针本身:
PyObject *
PyList_GetItem(PyObject *op, Py_ssize_t i)
{
if (!PyList_Check(op)) {
PyErr_BadInternalCall();
return NULL;
}
if (!valid_index(i, Py_SIZE(op))) {
PyErr_SetObject(PyExc_IndexError, &_Py_STR(list_err));
return NULL;
}
return ((PyListObject *)op) -> ob_item[i];
}
官方文档对此有一处针对 free-threaded 构建的明确警告:返回的借用引用在另一线程并发修改列表时可能失效,应优先使用 PyList_GetItemRef。即使是有 GIL 的构建,借用引用也只在"你保证列表和元素在此期间不被修改"的前提下安全;跨越任何 Python 调用(元素本身可能触发 __eq__、析构等钩子)时都应换成强引用。
PyList_GET_ITEM(无检查宏)
PyObject* PyList_GET_ITEM(PyObject *list, Py_ssize_t i);
与 PyList_GetItem 相同但不做任何错误检查,直接展开为 ob_item[i](Include/cpython/listobject.h)。仅在已确认类型与下标合法的热路径中使用,否则越界即产生未定义行为。同样适用于 free-threaded 构建中"借用引用可能失效"的警告。
写入元素:PyList_SetItem 的"窃取引用"语义与 PyList_SET_ITEM
PyList_SetItem
int PyList_SetItem(PyObject *list, Py_ssize_t index, PyObject *item);
将 list[index] 设为 item,成功返回 0,越界返回 -1 并设置 IndexError。
最重要的语义:该函数"窃取(steal)对 item 的引用,出错时也窃取。 Include/listobject.h 头文件注释原话:
WARNING: PyList_SetItem does not increment the new item's reference count, but does decrement the reference count of the item it replaces, if not nil. It does decrement the reference count if it is not inserted in the list.
也就是说有三种情形:
- 成功替换:列表接管
item的引用,原槽位对象的引用被释放(若原槽位非NULL); - 索引越界失败:
item的引用照样被释放,你不能再使用它; - 因此传入的
item应该是你拥有完整所有权的对象(refcnt 语义上"交给"列表),常见写法是PyList_SetItem(list, i, Py_NewRef(obj))。
Objects/listobject.c 的实现印证了这一点——所有失败路径都先 Py_XDECREF(newitem):
int
PyList_SetItem(PyObject *op, Py_ssize_t i, PyObject *newitem)
{
if (!PyList_Check(op)) {
Py_XDECREF(newitem);
PyErr_BadInternalCall();
return -1;
}
...
Py_BEGIN_CRITICAL_SECTION(self);
if (!valid_index(i, Py_SIZE(self))) {
Py_XDECREF(newitem); /* 出错也窃取 */
PyErr_SetString(PyExc_IndexError, "list assignment index out of range");
ret = -1;
goto end;
}
PyObject *tmp = self->ob_item[i];
FT_ATOMIC_STORE_PTR_RELEASE(self->ob_item[i], newitem);
Py_XDECREF(tmp); /* 释放被替换的旧对象 */
ret = 0;
end:
Py_END_CRITICAL_SECTION();
return ret;
}
Py_BEGIN_CRITICAL_SECTION / Py_END_CRITICAL_SECTION 是 free-threaded 构建中的 per-object lock 关键区(GIL 构建下为空操作),保证"替换 + 释放旧引用"是原子的。
PyList_SET_ITEM
void PyList_SET_ITEM(PyObject *list, Py_ssize_t i, PyObject *o);
PyList_SetItem 的宏形式,无错误检查,通常只用于填充一个尚未被外部看到的新列表(即 PyList_New 后的构造窗口)。文档指出两点:
- 在 debug 模式(
--with-pydebug)或--with-assertions构建下,越界检查以assert形式存在(对应 Include/cpython/listobject.h 中assert(0 <= index); assert(index < list->allocated);); - 它同样窃取
o的引用,但不会释放被替换槽位上的旧引用——若该位置已有对象,其引用会被泄漏。这与PyList_SetItem的关键区别,决定了它只适合填新列表:
static inline void
PyList_SET_ITEM(PyObject *op, Py_ssize_t index, PyObject *value) {
PyListObject *list = _PyList_CAST(op);
assert(0 <= index);
assert(index < list->allocated);
list->ob_item[index] = value; /* 无锁、无释放旧值 */
}
free-threaded 构建下该宏没有内部同步,官方文档建议:如果列表可能被其他线程共享,改用带 per-object lock 的 PyList_SetItem。
插入与追加:PyList_Insert 与 PyList_Append
PyList_Insert
int PyList_Insert(PyObject *list, Py_ssize_t index, PyObject *item);
在 index 之前插入 item,等价于 list.insert(index, item),成功返回 0,失败返回 -1 并设置异常。与 SetItem 不同,该函数不窃取引用,内部对 item 做一次 Py_NewRef。
Objects/listobject.c 中实际工作由 ins1 完成:先 list_resize(self, n+1) 扩容,然后负索引会被归一化(where < 0 时加 n,仍小于 0 则夹到 0),超出末尾则夹到 n,最后把 [where, n) 区间整体后移一格并写入新元素。整个过程包在 critical section 内。
PyList_Append
int PyList_Append(PyObject *list, PyObject *item);
等价于 list.append(item),不窃取引用。实现见 Objects/listobject.c:先 Py_NewRef(newitem),再走内部 _PyList_AppendTakeRef。扩容由 list_resize 负责(Objects/listobject.c),它采用 CPython 经典的过度分配策略:当新尺寸落在已分配容量的 [allocated/2, allocated] 区间内直接复用内存,否则按约 1.125 倍增长,从而摊薄 append 的均摊 O(1) 成本。
两个函数都要求参数确为 list 对象,否则返回 -1 并调用 PyErr_BadInternalCall(实现内部错误)。
切片与批量操作:PyList_GetSlice、PyList_SetSlice、PyList_Extend、PyList_Clear
PyList_GetSlice
PyObject* PyList_GetSlice(PyObject *list, Py_ssize_t low, Py_ssize_t high);
等价于 list[low:high],不支持负索引(从末尾索引),失败返回 NULL 并设置异常。Objects/listobject.c 的实现会对边界做夹取(clamp)而非报错:ilow < 0 归 0,ilow > len 归 len,ihigh 同样夹到 [ilow, len],因此 PyList_GetSlice(l, -1, 999) 会得到整个列表。整个读取在 Py_BEGIN_CRITICAL_SECTION(a) 临界区内完成,free-threaded 构建下防止并发修改撕裂快照。
PyList_SetSlice
int PyList_SetSlice(PyObject *list, Py_ssize_t low, Py_ssize_t high, PyObject *itemlist);
等价于 list[low:high] = itemlist。itemlist 可以为 NULL,表示用空列表替换(即切片删除)。成功返回 0,失败返回 -1,同样不支持负索引(实现位于 Objects/listobject.c)。
free-threaded 构建下的加锁规则在文档中有专门说明:当 itemlist 本身是 list 时,两个列表在整个操作期间都会被加锁;当 itemlist 是其他可迭代对象或 NULL 时,只锁目标 list。
PyList_Extend(3.13 新增)
int PyList_Extend(PyObject *list, PyObject *iterable);
等价于 list.extend(iterable) 或 list += iterable,即 PyList_SetSlice(list, PY_SSIZE_T_MAX, PY_SSIZE_T_MAX, iterable)。若 list 不是 list 对象则设置异常并返回 -1。实现见 Objects/listobject.c,内部复用与 += 相同的核心路径 _list_extend;抽象层 Objects/abstract.c 中的 PySequence_Extend 对真列表也是直接委托给 _PyList_Extend。
文档对 free-threaded 构建的加锁行为作了精确描述:当 iterable 是 list、set、dict 或其视图时,列表与源容器(或源 dict)都会被锁住;对于其他可迭代对象,只锁目标列表,iterable 本身可能被另一线程并发修改。
PyList_Clear(3.13 新增)
int PyList_Clear(PyObject *list);
等价于 list.clear() / del list[:],即 PyList_SetSlice(list, 0, PY_SSIZE_T_MAX, NULL)。非 list 对象时设置异常并返回 -1,成功返回 0。实现(Objects/listobject.c)在 critical section 内调用 list_clear 释放全部槽位引用。
原地排序与工具函数:PyList_Sort、PyList_Reverse、PyList_AsTuple
PyList_Sort
int PyList_Sort(PyObject *list);
原地排序,等价于 list.sort(),成功返回 0,失败返回 -1。实现位于 Objects/listobject.c。文档特别提示 free-threaded 构建下的一个微妙行为:通过 __lt__ 的元素比较可能执行任意 Python 代码,期间 per-object lock 可能被临时释放;对内置类型(str、int、float)的比较则不会释放锁。另外 PyListObject 结构注释提到 list.sort() 期间会把 allocated 置为 -1,从而在比较回调中检测到"排序期间列表被修改"并抛出异常。
PyList_Reverse
int PyList_Reverse(PyObject *list);
原地翻转,等价于 list.reverse(),成功返回 0,失败返回 -1(Objects/listobject.c)。
PyList_AsTuple
PyObject* PyList_AsTuple(PyObject *list);
返回一个包含列表内容的新元组,等价于 tuple(list);失败返回 NULL 并设置异常(Objects/listobject.c)。在需要把 list 结果传给只接受 tuple 参数的 C API(如 PyArg_ParseTuple 的 O! 组合)时常用。
Free-threaded(无 GIL)构建下的线程安全速览
3.13 的 List Objects 文档大量补充了 free-threaded 构建(Py_GIL_DISABLED)下的行为说明,可以归纳为三条规则:
- 借用引用不安全:
PyList_GetItem/PyList_GET_ITEM返回的借用引用可能因另一线程修改列表而失效,首选返回强引用的PyList_GetItemRef; PyList_SET_ITEM无内部同步:它只是裸指针写入,仅适用于"其他线程还拿不到该列表"的构造期;共享列表一律用带 per-object lock 的PyList_SetItem;- 复合操作持锁:
PyList_SetSlice、PyList_Extend、PyList_GetSlice等在 critical section 内执行,具体锁哪些对象(是否同时锁源容器)见前文各节说明。
从 Objects/listobject.c 的源码可以看到这套机制的落地:所有公开写操作都以 Py_BEGIN_CRITICAL_SECTION(obj) 开头、Py_END_CRITICAL_SECTION() 结尾;而读操作则先尝试无锁原子 fast path,失败再回退到持锁路径。在 GIL 构建下这些关键区是空操作,语义不变,因此同一份 C 扩展代码可以双构建通用。
引用语义速查表
| API | 返回/参数语义 | 越界行为 | free-threaded 建议 |
|---|---|---|---|
PyList_New(len) |
新列表强引用,槽位初始为 NULL | len<0 内部错误 | 构造期用 SET_ITEM 填充 |
PyList_Size / GET_SIZE |
只读,无引用变化 | 非 list 触发内部错误 | GET_SIZE 原子读,安全 |
PyList_GetItemRef(3.13) |
返回强引用,调用者 DECFREF | IndexError |
首选 API |
PyList_GetItem |
返回借用引用 | IndexError |
短窗口内使用,勿跨 Python 调用持有 |
PyList_GET_ITEM |
借用引用,无检查 | 未定义行为 | 热路径专用 |
PyList_SetItem |
窃取 item 引用(含出错路径),释放旧槽位 | IndexError,item 仍被释放 |
安全(内部持锁) |
PyList_SET_ITEM |
窃取引用,不释放被替换旧值 | assert(debug 构建) | 仅限私有新列表 |
PyList_Insert / Append |
不窃取,内部 NewRef | 成功返回 0 | 安全(内部持锁) |
PyList_GetSlice |
新列表强引用 | 边界被夹取,不报错 | 安全(快照持锁) |
PyList_SetSlice |
NULL 表示切片删除 |
边界被夹取 | list 源时双锁 |
PyList_Extend(3.13) |
不窃取 | 非 list 触发内部错误 | list/set/dict 源时双锁 |
PyList_Clear(3.13) |
无 | 非 list 触发内部错误 | 安全(内部持锁) |
PyList_Sort / Reverse |
原地操作 | 无 | 比较/回调期间锁可释放 |
PyList_AsTuple |
新元组强引用 | 无 | 无 |
验证与延伸阅读
- 官方 C API 文档原文:Doc/c-api/list.rst,本文所有函数签名、返回值约定与版本标注(
PyList_GetItemRef、PyList_Extend、PyList_Clear均为 3.13 新增)均以其为准; - 公共头文件:Include/listobject.h(含引用语义警告注释)与 Include/cpython/listobject.h(
PyListObject结构、PyList_GET_SIZE/PyList_SET_ITEM内联实现;注意PyList_GetItemRef的声明受Py_LIMITED_API >= 0x030d0000门控,旧稳定 ABI 下不可用); - 核心实现:Objects/listobject.c,覆盖 freelist 复用(
PyList_New)、freethreading 快慢路径(list_get_item_ref/list_item_impl)、过度分配扩容(list_resize)等; - C API 行为测试:Lib/test/test_capi/test_list.py,可对照本文各节结论回归验证;free-threaded 专项测试见 Lib/test/test_free_threading/test_list.py。
实战建议小结:日常 C 扩展中,创建列表用 PyList_New + PyList_SET_ITEM 快速填充、再整体交出;读取元素优先 PyList_GetItemRef 并及时 Py_DECREF;写入共享列表用 PyList_SetItem 并注意它是"窃取引用"语义;需要批量追加时,3.13+ 直接用 PyList_Extend,比循环 PyList_Append 更贴合 list.extend 的语义与加锁行为。
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