跳转到内容

切片

三个常被忽略的点:

  1. slice(a, b, c) 是一等公民对象,可以取名字,不用把数字散在代码里。
  2. 切片放在赋值号左边,能原地改可变序列,不用重建整个列表。
  3. a:b:c 只在 [] 里合法。seq[1:3] 实际调用的是 seq.__getitem__(slice(1, 3))。

这个约定不是随意定的,它带来三个很方便的性质:

>>> l = [10, 20, 30, 40, 50, 60]
>>> l[:2] # 只给 stop,长度一眼看出是 2
[10, 20]
>>> l[2:] # 从 2 切到底
[30, 40, 50, 60]
性质 说明
只看 stop 就知道长度 range(3) 和 l[:3] 都是 3 项
长度就是 stop - start 不用 ±1 修补
在任意位置一刀两断不重叠 l[:x] + l[x:] 正好拼回原序列,无重无漏

我把它写成了测试:

left, right = splitting_at(2, values)
assert len(left) + len(right) == len(values) # 不漏
assert left == [10, 20] # 不重
>>> s = 'bicycle'
>>> s[::3]
'bye'
>>> s[::-1]
'elcycib'
>>> s[::-2]
'eccb'

第 1 章那个 deck[12::13] 也是这个用法 —— 从下标 12 开始,每次跳 13 步, 正好把四张 A 都挑出来。

这是我最喜欢的一个技巧。假设要解析这种定长文本:

0.....6.................................40........52...55........
1909 Pimoroni PiBrella $17.50 3 $52.50

不要在每个地方写 line[40:52],把切片存成变量:

SKU = slice(0, 6)
DESCRIPTION = slice(6, 40)
UNIT_PRICE = slice(40, 52)
QUANTITY = slice(52, 55)
ITEM_TOTAL = slice(55, None)
for item in line_items:
print(item[UNIT_PRICE], item[DESCRIPTION])

读的时候不用去数「第 40 到第 52 个字符是什么来着」。跟电子表格里给单元格区域命名是一回事。

可变序列可以用切片做嫁接、切除、替换:

>>> l = list(range(10))
>>> l[2:5] = [20, 30] # 3 个位置换成 2 个元素,长度变了
>>> l
[0, 1, 20, 30, 5, 6, 7, 8, 9]
>>> del l[5:7] # 切片删除
>>> l
[0, 1, 20, 30, 5, 8, 9]
>>> l[3::2] = [11, 22] # 步长不为 1 时,个数必须刚好对上
>>> l
[0, 1, 20, 11, 5, 22, 9]
>>> l[2:5] = 100
TypeError: must assign iterable to extended slice
>>> l[2:5] = [100] # 想放一项,得写成列表
>>> l
[0, 1, 100, 22, 9]

[] 可以收逗号分隔的多个下标。a[i, j] 传进 __getitem__ 的其实是一个元组:

a[i, j] -> a.__getitem__((i, j))

NumPy 的二维数组就是这么工作的。a[m:n, k:l] 是一个二维切片。

...(三个英文句点,不是 Unicode 省略号 …)是 Python 的一个词法记号, 指向 Ellipsis 对象。NumPy 用它做多维切片的快捷写法:x[i, ...] 等价于 x[i, :, :, :]。

Python 内置类型里只有 memoryview 支持多维。标准库里我没找到别的用处 —— 这两个语法主要是给 NumPy 这类扩展留的口子。

  • test_splitting_has_no_overlap_and_no_gap —— 一刀两断不漏不重
  • test_assign_to_slice —— 四步操作之后的最终结果
  • test_assign_to_slice_needs_iterable —— 抛 TypeError
  • test_slice_object_is_hashable —— slice 可哈希,能当 dict 的键