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

简介: a[1:5:2] 传给 __getitem__ 的是一个 slice 对象,不是三个整数;真正的下标计算由 slice.indices(n) 完成。本文用探针类实测 key 类型、p[1,2] 收到 tuple、负步长 (5,-1,-1) 的哨兵 stop、step=0 的 ValueError,以及 10 万元素 timeit 对照:带步长切片 587µs 比全量拷贝 1787µs 更快,附自定义容器最小实现与五条排错清单

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

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

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

一、先看 slice 对象长什么样

>>> 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 原样打印出来:

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)

实测输出:

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 表示一个永远到不了的边界。

四、切片赋值与返回类型

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

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

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

>>> 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[:]」这条老经验,在这个量级上是有数据支撑的。

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

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(),比背「左闭右开」有用得多。

相关文章
|
8天前
|
JavaScript API iOS开发
DeepSeek Harness下载、安装、配置一篇搞定(2026整理,送常用插件)
DeepSeek Harness(DSH)是DeepSeek于2026年开源的MIT协议AI Agent框架,支持插件化组装模型、工具、沙箱与UI,兼容主流大模型。本文提供免Node.js的桌面端安装包(Win/macOS),含详细图文教程及热门插件推荐。
3483 34
|
7天前
|
Linux 开发工具 iOS开发
Codex保姆级教程(下载+安装+登录+使用,含所有版本,2026最新)
Codex是OpenAI推出的开源AI编程智能体,本地运行,可理解整个代码库,支持自然语言指令改代码、执行命令、跑测试、修Bug,兼容Win/macOS/Linux。(239字)
|
2天前
|
云安全 人工智能 安全
|
8天前
|
iOS开发 Docker MacOS
【2026实测】Docker Desktop下载、安装、汉化、配置教程(附官网安装包)
Docker Desktop 是 Docker 官方推出的桌面容器管理工具,集成引擎、CLI、图形界面与 Kubernetes,支持 Win/macOS/Linux。2026 最新版 4.88.1 强化 ARM 原生运行、镜像加速及资源可视化,附中文汉化与国内镜像配置指南。
1565 5
|
云安全 安全 网络安全
|
8天前
|
安全 算法 定位技术
《高性价比人生指南》完整原版:普通人用最少的资源,过好这一生的底层逻辑~How To Live Better
📖《高性价比人生指南》是一份基于实证、拒绝焦虑的生存手册,倡导以“人生时薪”为视角,重新计算时间、金钱与精力的成本与收益。它不承诺逆袭,只教人算清账:省下的钱若耗掉时间便亏;买体验比买物品更值;睡眠是最高杠杆的健康投资;关系贵在情绪价值而非表面热络;学习重在改变行为而非自我安慰。核心理念:用低成本、高回报的微小行动,换取长期的选择权——不是成为谁,而是掌控自己人生的主动权。
|
21天前
|
人工智能 API 开发工具
DeepSeek Harness 神插件:白嫖 13 家大厂模型
`dsh-our-free-model` 是 DeepSeek Harness 神级插件:零配置、零API Key、零账号,聚合13家大厂免费模型(含DeepSeek V4.1 Flash、Kimi K3等),支持本地网关、用量看板与自动降级,真正实现“白嫖即用”。
3728 55
|
13天前
|
人工智能 JSON Linux
【保姆级】ComfyUI部署教程(下载+安装+配置+工作流,2026最新)
ComfyUI 是一款开源节点式AI生成工具,支持图像、视频、音频及3D内容创作。通过拖拽连线组合模型加载、提示词、采样等节点,构建可复用、易修改的工作流,兼容SD1.5、SDXL、Flux、Qwen Image等主流模型。