news 2026/9/7 8:23:37

CPython C API 中的切片对象(Slice Objects)与 Ellipsis:从 PySlice_New 到 PySlice_AdjustIndices 的完整实现解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CPython C API 中的切片对象(Slice Objects)与 Ellipsis:从 PySlice_New 到 PySlice_AdjustIndices 的完整实现解析

CPython C API 中的切片对象(Slice Objects)与 Ellipsis:从 PySlice_New 到 PySlice_AdjustIndices 的完整实现解析

【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython

CPython 的切片对象是a[1:10:2]这类语法在 C 层的落地载体:当解释器编译切片表达式时,会构造一个slice对象,而 C 扩展模块若要支持“扩展切片”(extended slicing),则必须通过 C API 读取并解释start/stop/step三个分量。本文基于 CPython 官方文档 Doc/c-api/slice.rst 中列出的全部 API(PySlice_TypePySlice_CheckPySlice_NewPySlice_GetIndicesPySlice_GetIndicesExPySlice_UnpackPySlice_AdjustIndicesPyEllipsis_TypePy_Ellipsis),结合 Objects/sliceobject.c 与 Include/sliceobject.h 的源码,逐一讲解每个函数的语义、返回值约定、历史演变(3.2 / 3.6.1 / 3.7 / 3.12 的关键变更),以及为什么现代 C 扩展应当用PySlice_Unpack+PySlice_AdjustIndices组合取代PySlice_GetIndicesEx

切片对象的类型与判定:PySlice_Type 与 PySlice_Check

文档首先给出两个类型级接口:

  • PyTypeObject PySlice_Type:slice 对象的类型对象,与 Python 层的内置slice类是同一个对象;
  • int PySlice_Check(PyObject *ob):判断ob是否为 slice 对象,ob不得为NULL,该函数“总是成功”(即不会失败地返回错误)。

从源码结构看,这两者在 C 层的对应关系非常直接。Include/sliceobject.h 中声明了PySlice_Type,并把PySlice_Check定义为一个基于类型指针比较的宏:

PyAPI_DATA(PyTypeObject) PySlice_Type; PyAPI_DATA(PyTypeObject) PyEllipsis_Type; #define PySlice_Check(op) Py_IS_TYPE((op), &PySlice_Type)

也就是说PySlice_Check只比较op->ob_type是否等于&PySlice_Type,它不检查子类——如果一个 C 扩展创建并注册了slice的子类,PySlice_Check对其实例会返回 0。需要支持子类的场景应改用PyObject_TypeCheck

真正的类型对象在 Objects/sliceobject.c 中定义,名为"slice",关键槽位包括:

  • tp_dealloc = slice_dealloc:先PyObject_GC_UnTrack,再依次Py_DECREF三个成员,最后归还 freelist;
  • tp_hash = slice_hash:借鉴自 tuple 的 xxhash 变体,把start/stop/step三个分量的哈希按acc += lane * PRIME2; rotate; acc *= PRIME1的顺序折叠,若结果恰为(Py_uhash_t)-1则替换为1546275796
  • tp_richcompare = slice_richcompare:先把两个切片各自打包成三元素元组(start, stop, step)再逐元素比较,这与 Python 层slice(1,2,3) == slice(1,2,3)的行为一致;
  • tp_traverse = slice_traverse:访问全部三个成员,使切片对象参与 GC(因为 start/stop/step 是任意 Python 对象,可能形成引用环,类型标志带有Py_TPFLAGS_HAVE_GC);
  • tp_new = slice_new:只接受位置参数(_PyArg_NoKeywords),参数范围 1~3 个,slice(stop)slice(start, stop[, step])两种形式均支持——单参数时把该值当作stop并把start置为None,注释说明这是为了与range()保持相似。

切片对象的内存布局定义在 Include/cpython/sliceobject.h(仅限内部 API,非Py_LIMITED_API下可用):

typedef struct { PyObject_HEAD PyObject *start, *stop, *step; /* not NULL */ } PySliceObject;

头文件注释解释了设计决策:start/stop/step允许是任意 Python 类型(例如 NumPy 数组下标),Py_None表示对应分量被省略,且三个指针永远不为NULL

创建切片:PySlice_New 的 NULL 语义与引用计数约定

PyObject *PySlice_New(PyObject *start, PyObject *stop, PyObject *step);

文档约定:三个参数直接成为切片对象的同名属性值;任一参数可以为NULL,此时对应属性使用None填充;分配失败时返回NULL并设置异常。

Objects/sliceobject.c 的实现印证了这一点:

PyObject * PySlice_New(PyObject *start, PyObject *stop, PyObject *step) { if (step == NULL) step = Py_None; if (start == NULL) start = Py_None; if (stop == NULL) stop = Py_None; return _PyBuildSlice_ConsumeRefs(Py_NewRef(start), Py_NewRef(stop), Py_NewRef(step)); }

两个值得注意的实现细节:

  1. 不转移引用PySlice_New对每个参数先Py_NewRef再“消费”新引用,调用方保留自己原有的引用——这与“返回新对象、参数为 borrowed 语义”的公共 API 约定一致;
  2. freelist 加速:内部助手_PyBuildSlice_ConsumeRefs(Objects/sliceobject.c)先尝试从模块级 freelist 弹出一个已释放的PySliceObject_Py_FREELIST_POP),命中时省去一次 GC 对象分配,否则才走PyObject_GC_New。切片是解释器运行期的高频对象,freelist 在此处能显著降低分配/释放开销。

另外源码中还提供了仅内部使用的_PySlice_FromIndices(Objects/sliceobject.c),由两个Py_ssize_t直接构造slice(start, stop),供解释器内部路径使用。

旧接口 PySlice_GetIndices:为什么文档劝你“别用”

int PySlice_GetIndices(PyObject *slice, Py_ssize_t length, Py_ssize_t *start, Py_ssize_t *stop, Py_ssize_t *step);

文档描述:假设序列长度为length,从切片对象取出 start/stop/step;超过length的索引会被当作错误。成功返回0,失败返回-1——但注意区分:只有当某个分量既非None又无法转换为整数时,-1才伴随异常;越界返回的-1不带异常的。文档原话是 “You probably do not want to use this function.”,并在 3.2 把slice参数类型从PySliceObject*放宽为PyObject*(允许切片子类)。

Objects/sliceobject.c 的实现精确对应文档的语义:

int PySlice_GetIndices(PyObject *_r, Py_ssize_t length, Py_ssize_t *start, Py_ssize_t *stop, Py_ssize_t *step) { PySliceObject *r = (PySliceObject*)_r; if (r->step == Py_None) { *step = 1; } else { if (!PyLong_Check(r->step)) return -1; *step = PyLong_AsSsize_t(r->step); } /* start / stop 同理;负索引加 length */ if (*stop > length) return -1; /* 越界 → 错误,不裁剪 */ if (*start >= length) return -1; if (*step == 0) return -1; return 0; }

它的缺点一目了然:只接受PyLong(不支持带__index__的自定义整数类型)、越界即报错而不是裁剪、无法返回切片长度。因此文档将其定位为历史遗留,现代扩展应使用下面的替代方案。

PySlice_GetIndicesEx:推荐接口及其弃用史

int PySlice_GetIndicesEx(PyObject *slice, Py_ssize_t length, Py_ssize_t *start, Py_ssize_t *stop, Py_ssize_t *step, Py_ssize_t *slicelength);

文档称其为PySlice_GetIndices的可用替代:除取出 start/stop/step 外,还按正常切片的方式裁剪越界索引,并把所得切片长度写入slicelength;成功返回0,出错返回-1必定设置异常。版本史同样值得记录:

  • 3.2slice参数类型由PySliceObject*改为PyObject*
  • 3.6.1:在Py_LIMITED_API未设置、或取值于[0x03050400, 0x03060000)、或≥ 0x03060100时,PySlice_GetIndicesEx被实现为基于PySlice_UnpackPySlice_AdjustIndices,此时startstopstep三个实参会被求值多次(传参时应避免带副作用的表达式);
  • 3.6.1:当Py_LIMITED_API取值小于0x03050400、或位于[0x03060000, 0x03060100)时,该函数进入弃用状态。

当前仓库的头文件 Include/sliceobject.h 完整保留了这一历史结构:函数声明带Py_DEPRECATED(3.7)标注,而那个“宏版本”以#define PySlice_GetIndicesEx(...)形式存在,条件与文档一致:

Py_DEPRECATED(3.7) PyAPI_FUNC(int) PySlice_GetIndicesEx(PyObject *r, Py_ssize_t length, Py_ssize_t *start, Py_ssize_t *stop, Py_ssize_t *step, Py_ssize_t *slicelength); #if !defined(Py_LIMITED_API) || (Py_LIMITED_API+0 >= 0x03050400 && Py_LIMITED_API+0 < 0x03060000) || Py_LIMITED_API+0 >= 0x03060100 #define PySlice_GetIndicesEx(slice, length, start, stop, step, slicelen) ( \ PySlice_Unpack((slice), (start), (stop), (step)) < 0 ? \ ((*(slicelen) = 0), -1) : \ ((*(slicelen) = PySlice_AdjustIndices((length), (start), (stop), *(step))), \ 0)) PyAPI_FUNC(int) PySlice_Unpack(PyObject *slice, Py_ssize_t *start, Py_ssize_t *stop, Py_ssize_t *step); PyAPI_FUNC(Py_ssize_t) PySlice_AdjustIndices(Py_ssize_t length, Py_ssize_t *start, Py_ssize_t *stop, Py_ssize_t step); #endif

函数式实现位于 Objects/sliceobject.c,先#undef掉宏再给出等价的两步实现。

面向现代扩展的推荐组合:PySlice_Unpack + PySlice_AdjustIndices

文档在PySlice_GetIndicesEx条目下给出了一条重要的 note:该函数对可伸缩(resizable)序列并不安全。原因是旧写法把“解包”和“按长度裁剪”耦合在一次性调用里,无法在取数前刷新长度。文档给出的等价替换模式是:

/* 旧写法(不安全) */ if (PySlice_GetIndicesEx(slice, length, &start, &stop, &step, &slicelength) < 0) { // return error } /* 新写法(推荐) */ if (PySlice_Unpack(slice, &start, &stop, &step) < 0) { // return error } slicelength = PySlice_AdjustIndices(length, &start, &stop, step);

PySlice_Unpack:把分量安全地收窄到 Py_ssize_t 范围

int PySlice_Unpack(PyObject *slice, Py_ssize_t *start, Py_ssize_t *stop, Py_ssize_t *step);

(3.6.1 新增。)从切片对象提取 start/stop/step 为 C 整数,错误时返回-1并设置异常,成功返回0。它处理了 64 位整数“溢出收窄”的边界:大于PY_SSIZE_T_MAX的值被静默收窄到PY_SSIZE_T_MAXstart/stop中小于PY_SSIZE_T_MIN的值提升为PY_SSIZE_T_MINstep中小于-PY_SSIZE_T_MAX的值提升为-PY_SSIZE_T_MAX

Objects/sliceobject.c 的实现补充了文档未完全展开的语义:

  • step == None时默认*step = 1step显式转换后为 0 时抛出ValueError: "slice step cannot be zero"
  • start/stopNone时不取 0/length,而是取极端哨兵值:step < 0*start = PY_SSIZE_T_MAX*stop = PY_SSIZE_T_MIN,否则*start = 0*stop = PY_SSIZE_T_MAX——把“省略”编码为超出任何实际长度的边界,交给下一步统一裁剪;
  • 分量转换由_PyEval_SliceIndex完成,即接受int及任何实现了__index__的对象,失败时由该助手设置TypeError(这与 Python 层slice indices must be integers or None or have an __index__ method的报错来源一致,参见同文件 Objects/sliceobject.c 中slice.indices()路径的evaluate_slice_index);
  • 源码中专门注释了step < -PY_SSIZE_T_MAX收窄到-PY_SSIZE_T_MAX的动机:避免反转切片时step = -step触发INT_MIN取负的未定义行为(static_assert在编译期保证该区间合法)。

PySlice_AdjustIndices:无异常、无 Python 调用的纯裁剪

Py_ssize_t PySlice_AdjustIndices(Py_ssize_t length, Py_ssize_t *start, Py_ssize_t *stop, Py_ssize_t step);

(3.6.1 新增。)给定序列长度,就地裁剪start/stop,返回切片长度;总是成功、从不调用 Python 代码,因此可以在“取长度”与“裁剪”之间安全地插入其他操作(这正是它对可伸缩序列友好的原因)。

Objects/sliceobject.c 的核心逻辑:

if (*start < 0) { *start += length; if (*start < 0) *start = (step < 0) ? -1 : 0; /* 下界裁剪 */ } else if (*start >= length) { *start = (step < 0) ? length - 1 : length; /* 上界裁剪 */ } /* *stop 同理 */

两个方向依赖step的符号,这是实现中最容易出错的部分(源码注释直言 “this is harder to get right than you might think”):

  • 正向切片step > 0):越界的start被推到length、越界的stop被推到length,负到穿底的索引落到0
  • 反向切片step < 0):越界的start落到length - 1(最后一个元素),负到穿底的索引落到-1,越界的stop落到length - 1、穿底则到-1
  • 长度计算用整除公式,反向为(start - stop - 1) / (-step) + 1,正向为(stop - start - 1) / step + 1,区间不成立时返回0

这一行为与 Python 层slice.indices(len)完全一致:后者由 Objects/sliceobject.c 的slice_indices实现,内部走_PySlice_GetLongIndices用任意精度整数做同样的边界比较(因为len本身可以是任意大的int),最后Py_BuildValue("(NNN)", start, stop, step)返回三元组。Python 层的完整行为(构造、repr、比较、哈希、indices、pickle、循环引用等)由 Lib/test/test_slice.py 中的test_constructortest_indicestest_hashtest_cmptest_memberstest_pickletest_cycle等用例覆盖,可作为 C 层语义的对照基准。

Ellipsis Object:PyEllipsis_Type 与 Py_Ellipsis 单例

文档的最后一节转向...对象(与切片同属 Objects/sliceobject.c 这个文件,历史原因使其与 slice 放在一起):

  • PyTypeObject PyEllipsis_TypeEllipsis对象的类型,等价于 Python 层的types.EllipsisType
  • PyObject *Py_Ellipsis:唯一的Ellipsis对象,没有方法;与Py_None一样是永生(immortal)单例。3.12 起Py_Ellipsis变为 immortal。

源码层面可以确认 immortal 的落地方式。Objects/sliceobject.c 定义了类型PyEllipsis_Typetp_name"ellipsis"),其tp_newellipsis_new)拒绝任何实参——EllipsisType takes no arguments——并直接返回Py_Ellipsis;单例实体是_Py_EllipsisObject,而tp_deallocellipsis_dealloc)的注释写得很直白:正常路径永远不会释放它,但万一人工把引用计数减到 0,与其 SEGV,不如借助 immortal 机制重置引用计数(_Py_SetImmortal)。它仅暴露一个__reduce__方法(返回字符串"Ellipsis"),使 pickle 可以还原该单例。

Include/sliceobject.h 对Py_Ellipsis的取值还区分了 ABI 版本:

#if defined(Py_LIMITED_API) && Py_LIMITED_API+0 >= 0x030D0000 # define Py_Ellipsis Py_GetConstantBorrowed(Py_CONSTANT_ELLIPSIS) #else # define Py_Ellipsis (&_Py_EllipsisObject) #endif

即在 3.13 及以上的受限 ABI(stable ABI)下通过Py_GetConstantBorrowed借用常量(符合 stable ABI 不直接暴露全局PyObject地址的约束),其余版本直接取静态对象的地址。对使用者的实践含义是:Py_Ellipsis是 borrowed 引用,不要Py_DECREF它;在 C 扩展中判断“用户是否省略了下标”时(例如np.ndarray风格的obj[...]处理),比较arg == Py_Ellipsis是标准做法。

选型小结:C 扩展处理切片的正确姿势

把 Doc/c-api/slice.rst 的全部接口与 Objects/sliceobject.c 的实现对照后,可以得到一张清晰的选型表:

接口语义要点状态与适用场景
PySlice_Typeslice类型对象,同 Python 层slice基础常量
PySlice_Check(ob)精确类型比较,要求ob != NULL,总是成功需要支持子类时用PyObject_TypeCheck
PySlice_New(start, stop, step)任一参数可为NULL(以None填充);不转移引用;失败返回NULL并设置异常构造切片的通用入口
PySlice_GetIndices(...)越界返回不带异常的-1;只认PyLong文档明确不推荐,历史接口
PySlice_GetIndicesEx(...)越界裁剪并返回切片长度;出错必带异常3.7 起弃用;新版下是Unpack+AdjustIndices的宏,实参会被求值多次
PySlice_Unpack(...)分量收窄到Py_ssize_t范围,None编码为极端哨兵值,0 step 抛ValueError3.6.1 新增,推荐
PySlice_AdjustIndices(...)纯裁剪 + 长度计算,总是成功、无 Python 调用3.6.1 新增,推荐;对可伸缩序列安全
PyEllipsis_Type/Py_EllipsisEllipsis类型与 immortal 单例(3.12 起)borrowed 引用,勿 DECREF

推荐的扩展切片处理骨架即文档给出的两段式写法:先PySlice_Unpack完成“可能失败、可能抛异常”的解析,再在拿到最新序列长度后调用无副作用的PySlice_AdjustIndices完成裁剪。这套组合既保留了 Python 层slice.indices()的完整语义(负索引、越界裁剪、反向步进),又规避了旧接口对可伸缩序列和超大整数索引的隐患,是当前 CPython C API 中处理切片的规范做法。

【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 8:20:11

FOC电流采集性能优化实战:从软件等待到硬件协同

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 8:19:23

千牛深层iframe里的验证组件:逐层穿透定位的技术方案

千牛深层iframe里的验证组件&#xff1a;逐层穿透定位的技术方案 前端工程师有个祖传手艺&#xff1a;把关键组件嵌进iframe&#xff0c;嵌他个三层四层。对用户无感&#xff0c;对自动化是迷宫——元素明明就在页面上&#xff0c;定位器就是够不着。 千牛工作台的一些功能模…

作者头像 李华
网站建设 2026/9/7 8:19:12

独立向量分析IVA的MATLAB实现:解决多被试EEG源对齐难题

简介&#xff1a;独立向量分析&#xff08;IVA&#xff09;的MATLAB实现代码包&#xff0c;定位服务于音频信号处理与盲源分离方向的研究者、工程师及相关专业学生&#xff0c;适用于从多麦克风混合录音中恢复多个独立声源的场景。压缩包体积仅3KB&#xff0c;共包含3个.m源文件…

作者头像 李华
网站建设 2026/9/7 8:18:34

比特彗星绿色版全功能解锁与下载提速实战指南

简介&#xff1a;这是一款面向BT/PT下载用户的全功能解锁便携版比特彗星&#xff0c;解除了官方免费版的功能限制&#xff0c;并省去安装步骤&#xff0c;适合追求高速下载与纯净环境的中高级网络用户。压缩包共169个文件&#xff0c;总大小仅18.64MB&#xff0c;包含11个exe主…

作者头像 李华
网站建设 2026/9/7 8:18:26

AI绘画模型实战:第2个闪耀迪迦的技术解析与应用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 8:17:19

InoProShop中EtherCAT总线配置从入门到故障排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华