Python 的 a[1:5:2] 到底切出了什么?拆开 slice 对象、__getitem__ 与负步长那次反向

写 Python 的人几乎天天用 a[1:5:2],但真要问「这三个数最后去哪了」,多数人答不上来。容易误以为是「取下标 1、3」,或者「传三个参数给某个函数」。都不是。

一句话结论:a[1:5:2] 会先造出一个 slice 对象,再把这个对象整个交给 __getitem__;真正的下标计算由 slice.indices(n) 完成,容器拿到的是 (start, stop, step) 三元组。

下面全部数据来自本机 CPython 3.13.14 实测。

一、先看 slice 对象长什么样

python 复制代码
>>> s = slice(1, 5, 2)
>>> s.start, s.stop, s.step
(1, 5, 2)
>>> s.indices(7)      # 长度 7 的序列
(1, 5, 2)
>>> s.indices(3)      # 长度只有 3,自动裁剪
(1, 3, 2)
>>> slice(None, None, -1).indices(6)
(5, -1, -1)

indices(n) 是整套机制里最该记住的一个方法:它把「缺省值、负数、越界」全部归一化成可以直接喂给 range() 的三元组。注意 indices(3) 把 stop 从 5 裁成了 3------所以切片越界不报错,根源在这里,不在 __getitem__。

slice 对象还是可哈希的(实测 slice(1,2).__hash__ 不为 None),这意味着它可以当字典的 key。

二、getitem 收到的到底是什么

写一个探针类,把收到的 key 原样打印出来:

python 复制代码
class Probe:
    def __getitem__(self, k):
        print(type(k).__name__, repr(k),
              k.indices(10) if isinstance(k, slice) else '-')
        return 42

p = Probe()
p[2]        # int    2
p[1:8:3]    # slice  slice(1, 8, 3)      -> (1, 8, 3)
p[::2]      # slice  slice(None, None, 2) -> (0, 10, 2)
p[1, 2]     # tuple  (1, 2)

实测输出:

less 复制代码
p[2]      -> __getitem__ got: int    2
p[1:8:3]  -> __getitem__ got: slice  slice(1, 8, 3)  | indices(10) = (1, 8, 3)
p[::2]    -> __getitem__ got: slice  slice(None, None, 2) | indices(10) = (0, 10, 2)
p[1,2]    -> __getitem__ got: tuple  (1, 2)

三点值得记:

  1. 切片只传一个对象,不是三个位置参数;
  2. a[1, 2] 传的是 tuple,这是 numpy 那类库能区分「多维索引」和「切片」的原因;
  3. 缺省位置是 None,不是 0 也不是 len(a)------归一化是 indices() 的事。

我猜把 key 设计成对象而不是三个整数,主要就是为了给第三方容器留解释权:numpy 可以把 tuple 里每个元素再解释成各轴的 slice,而内置 list 只认单个 slice 或 int。语言层面不替容器做决定。

三、常见写法对照表

写法 传给 __getitem__ 的 key indices(10) 结果(以 list(range(10)) 为例)
a[2] int 2 --- [2] 里的元素 2
a[1:5] slice(1,5,None) (1,5,1) [1,2,3,4]
a[1:5:2] slice(1,5,2) (1,5,2) [1,3]
a[::2] slice(None,None,2) (0,10,2) [0,2,4,6,8]
a[::-1] slice(None,None,-1) (9,-1,-1) [9,8,...,0]
a[3:1:-1] slice(3,1,-1) (3,1,-1) [3,2]
a[1, 2] tuple (1,2) --- 由容器自定义(list 会报 TypeError)
a[::0] slice(None,None,0) ValueError ValueError: slice step cannot be zero

负步长那两行最容易看错。slice(None,None,-1).indices(6) 返回的是 (5, -1, -1):start 变成最后一个下标,stop 变成 -1。这里的 -1 是哨兵值 ,不是「下标 -1」------因为负步长下 range 要一路减到 -1 之前才停,用正常的 -1 会和「最后一个元素」的真实下标撞车,所以 CPython 用 -1 表示一个永远到不了的边界。

四、切片赋值与返回类型

python 复制代码
>>> a = list(range(10))
>>> a[1:5:2] = ['X', 'Y']
>>> a
[0, 'X', 2, 'Y', 4, 5, 6, 7, 8, 9]

带步长的切片赋值要求右侧长度严格等于切片元素个数,否则 ValueError。这个限制来源于「替换后不能改变后面元素的下标」------跳跃式切片如果允许伸缩,整个列表的布局都要重排。

返回类型也别想当然,实测:

python 复制代码
>>> type('abcdef'[1:5:2]).__name__   # 'str'
>>> type((1,2,3,4)[::2]).__name__   # 'tuple'

切片返回的是同类型的新对象 ,不是 list。这也是 a[:] 能当「浅拷贝」用的原因。

五、10 万元素的性能账

timeit,200 次取均值,a = list(range(100000)):

操作 单次耗时
a[1:99999:2] 562.8 µs
a[::2] 587.1 µs
list(a) 1526.4 µs
a[:] 1786.9 µs

有意思的是带步长的切片反而比全量拷贝快:a[::2] 只搬一半元素,a[:] 要搬 10 万个指针。list(a) 比 a[:] 略快,因为前者走的是迭代器协议、后者走切片协议。所以「复制列表别用 a[:]」这条老经验,在这个量级上是有数据支撑的。

六、自定义类支持切片的最小实现

python 复制代码
class Seq:
    def __init__(self, data): self._d = list(data)
    def __len__(self): return len(self._d)
    def __getitem__(self, k):
        if isinstance(k, slice):
            start, stop, step = k.indices(len(self._d))
            return type(self)(self._d[start:stop:step])
        return self._d[k]        # 交给 list 处理负数 / 越界

关键是 永远用 k.indices(len(self)) 归一化 ,不要自己写 if start < 0 那一堆判断------负数、越界、负步长、None 的组合情况比想象中多,自己写十有八九漏一种。

七、排错清单

  1. ValueError: slice step cannot be zero :a[::0] 或 slice(1,5,0).indices(n)。步长不能为 0,构造 slice 时不报错,调用 indices() 才报。
  2. TypeError: unhashable type 反过来遇到「slice 能当 key」 :别惊讶,slice 是可哈希的。
  3. 负步长取不到第一个元素 :a[3:0:-1] 不含 a[0],想含就写 a[3::-1],让 stop 保持 None。
  4. 多维索引报错 :list 收到 a[1,2] 这个 tuple 会抛 TypeError,只有实现了多维语义的容器(numpy 等)才认。
  5. a[:] 是浅拷贝:嵌套列表里改内层对象,原列表照样变。

一句话收尾:切片不是语法糖,是一套「先造描述对象、再由容器自己解释」的协议。理解 slice.indices(),比背「左闭右开」有用得多。

相关推荐
wuyk55530 分钟前
PyQt6-第15讲-数据可视化QChart
python·ui·信息可视化·pyqt
天天被压力1 小时前
【A股分钟级高频数据完全手册 #04】指数分钟级行情数据接口:沪深指数Pro版最新分时取数
java·人工智能·python
日日大王学编程1 小时前
2026主流AI Agent产品分类、核心差异与技术特性全解析
人工智能·后端·python
天天被压力1 小时前
【A股分钟级高频数据完全手册 #03】股票历史1分钟K线数据接口怎么用:Pro版批量拉取与本地缓存
java·人工智能·python
武天旭16 小时前
AI安全--AI大模型的毒性偏见与安全对齐实践
人工智能·python·深度学习
视觉小萌新17 小时前
AI Class 3 · Chapter 2-3:函数、异常、文件读写+JSON / JSONL 读写(小白版)
人工智能·python·json
小小张说故事18 小时前
Python 的 obj.x 到底按什么顺序找?拆开数据描述符、实例字典和 __getattr__ 那条优先级链
python
用户114818678948419 小时前
水印的实现方式逻辑
前端·python
贾公子19 小时前
从单条内容到账号维度——AIGC 风险评分的工程实现
后端·python