首页
/ CPython List Objects C API 深度解析:从 PyList_New 到 PyList_AsTuple 的引用语义与实现细节

CPython List Objects C API 深度解析:从 PyList_New 到 PyList_AsTuple 的引用语义与实现细节

2026-09-04 21:17:47作者:秋阔奎Evelyn

本文围绕 CPython 官方 C API 文档中的 List Objects 一章,系统讲解 PyListObject 类型及其配套的 PyList_* 系列 C 函数:类型检查、创建、读写元素、切片操作、原地排序等。文中每个 API 都会结合 Objects/listobject.cInclude/listobject.h 的源码印证其行为细节、引用计数语义(尤其是"窃取引用"的陷阱)以及 free-threaded(无 GIL)构建下的线程安全注意事项,帮助 C 扩展开发者写出正确且高效的 list 操作代码。

类型系统:PyListObject 与 PyList_Type

列表对象是 CPython 中最常用的可变序列类型。C API 文档 Doc/c-api/list.rst 首先定义了两个核心符号:

  • PyListObject:表示 Python 列表对象的 PyObject 子类型;
  • PyList_TypePyTypeObject 实例,代表 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_SetItemPyList_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;
}

此外还可以观察到两个实现细节:

  1. 对象 freelistPyList_New 优先从进程内 freelist(_Py_FREELIST_POP)复用刚释放的 PyListObject,减少 GC 跟踪对象的分配开销;
  2. 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_SIZEInclude/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_DECREFindex 必须非负,不支持从列表末尾索引;越界(< 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_refObjects/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.

也就是说有三种情形:

  1. 成功替换:列表接管 item 的引用,原槽位对象的引用被释放(若原槽位非 NULL);
  2. 索引越界失败:item 的引用照样被释放,你不能再使用它;
  3. 因此传入的 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.hassert(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] = itemlistitemlist 可以为 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 构建的加锁行为作了精确描述:当 iterablelistsetdict 或其视图时,列表与源容器(或源 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 可能被临时释放;对内置类型(strintfloat)的比较则不会释放锁。另外 PyListObject 结构注释提到 list.sort() 期间会把 allocated 置为 -1,从而在比较回调中检测到"排序期间列表被修改"并抛出异常。

PyList_Reverse

int PyList_Reverse(PyObject *list);

原地翻转,等价于 list.reverse(),成功返回 0,失败返回 -1Objects/listobject.c)。

PyList_AsTuple

PyObject* PyList_AsTuple(PyObject *list);

返回一个包含列表内容的新元组,等价于 tuple(list);失败返回 NULL 并设置异常(Objects/listobject.c)。在需要把 list 结果传给只接受 tuple 参数的 C API(如 PyArg_ParseTupleO! 组合)时常用。

Free-threaded(无 GIL)构建下的线程安全速览

3.13 的 List Objects 文档大量补充了 free-threaded 构建(Py_GIL_DISABLED)下的行为说明,可以归纳为三条规则:

  1. 借用引用不安全PyList_GetItem / PyList_GET_ITEM 返回的借用引用可能因另一线程修改列表而失效,首选返回强引用的 PyList_GetItemRef
  2. PyList_SET_ITEM 无内部同步:它只是裸指针写入,仅适用于"其他线程还拿不到该列表"的构造期;共享列表一律用带 per-object lock 的 PyList_SetItem
  3. 复合操作持锁PyList_SetSlicePyList_ExtendPyList_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_GetItemRefPyList_ExtendPyList_Clear 均为 3.13 新增)均以其为准;
  • 公共头文件:Include/listobject.h(含引用语义警告注释)与 Include/cpython/listobject.hPyListObject 结构、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 的语义与加锁行为。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
docsdocs
暂无描述
Markdown
889
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341