切片
切片(slice)用于从有序序列中取出一段数据。它不仅能截取列表或字符串,还能跳跃取值、反转序列,以及批量修改列表。
切片最重要的规则是:包含起点 start,不包含终点 stop。这种“左闭右开”的设计让片段长度和拼接都很容易计算。
本文示例适用于 Python 3。学完后,你应该能够:
- 读懂
sequence[start:stop:step]; - 正确处理省略边界、负数索引和负步长;
- 区分索引访问、普通切片与切片赋值;
- 理解切片产生的复制及其性能影响;
- 使用
slice对象复用切片规则。
基本语法
完整的切片语法是:
sequence[start:stop:step]
start:起始索引,包含该位置;stop:结束索引,不包含该位置;step:步长,表示每次移动多少个位置。
三个参数都可以省略:
letters = ["A", "B", "C", "D", "E", "F"]
print(letters[1:4]) # ['B', 'C', 'D']
print(letters[:3]) # ['A', 'B', 'C']
print(letters[3:]) # ['D', 'E', 'F']
print(letters[:]) # ['A', 'B', 'C', 'D', 'E', 'F']
当 step 为正数或省略时,默认从左向右取值:
| 写法 | 等价理解 | 结果 |
|---|---|---|
letters[1:4] | 从索引 1 取到索引 4 之前 | ['B', 'C', 'D'] |
letters[:3] | 从开头取到索引 3 之前 | ['A', 'B', 'C'] |
letters[3:] | 从索引 3 取到末尾 | ['D', 'E', 'F'] |
letters[:] | 从开头取到末尾 | 全部元素 |
step 不能为 0,否则会抛出 ValueError:
# letters[::0] # ValueError: slice step cannot be zero
为什么终点不包含在结果中
左闭右开的规则有三个直接好处:
numbers = [0, 1, 2, 3, 4, 5]
part = numbers[2:5]
print(part) # [2, 3, 4]
print(len(part)) # 3,正好是 5 - 2
print(numbers[:2] + numbers[2:]) # 还原原列表
对于步长为 1,并且满足 0 <= a <= b <= len(sequence) 的切片:
sequence[:n]恰好取得前n个元素;sequence[a:b]的长度是b - a;sequence[:n]和sequence[n:]能在同一位置无缝分割序列。
索引和切片的区别
索引访问只取得一个元素,切片会取得一个同类型的新序列:
languages = ["Python", "Go", "Dart"]
print(languages[1]) # Go,类型是 str
print(languages[1:2]) # ['Go'],类型是 list
它们处理越界的方式也不同:
print(languages[99:100]) # [],切片边界会被限制在有效范围内
# print(languages[99]) # IndexError: list index out of range
切片适合“不足指定长度也可以”的场景,例如安全地取得前 10 项;如果必须存在某个元素,则应使用索引,让无效数据尽早暴露。
负数索引
负数索引从末尾计数:-1 是最后一个元素,-2 是倒数第二个元素。
text = "Python"
print(text[-1]) # n
print(text[-4:-1]) # tho
print(text[-3:]) # hon
print(text[:-2]) # Pyth
可以把负数边界粗略理解为 len(sequence) + index。例如,长度为 6 的字符串中,索引 -4 对应索引 2。
切片边界超出范围时会被自动收缩,而不是报错:
print(text[-100:100]) # Python
使用步长跳跃取值
省略 step 时,其默认值为 1。指定更大的正数可以间隔取值:
numbers = list(range(10))
print(numbers[::2]) # [0, 2, 4, 6, 8]
print(numbers[1::2]) # [1, 3, 5, 7, 9]
print(numbers[1:8:3]) # [1, 4, 7]
读取 numbers[1:8:3] 时,可以按下面的顺序理解:
- 从索引
1开始; - 每次向右移动
3个位置; - 到达或越过索引
8时停止,索引8本身不参与结果。
负步长和反向切片
负步长表示从右向左移动:
letters = "ABCDEFG"
print(letters[::-1]) # GFEDCBA
print(letters[5:1:-1]) # FEDC
print(letters[6:0:-2]) # GEC
当 step 为负数时,仍然包含 start、不包含 stop,但取值方向变为从右向左。此时 start 必须位于 stop 的右侧,否则结果为空:
print(letters[1:5:-1]) # '',无法按向左的方向从 1 到达 5
print(letters[5:1]) # '',默认步长向右,无法从 5 到达 1
省略边界时,Python 会根据步长方向选择默认值:
| 步长 | 省略 start | 省略 stop |
|---|---|---|
| 正数 | 从第一个元素开始 | 到最后一个元素之后结束 |
| 负数 | 从最后一个元素开始 | 到第一个元素之前结束 |
因此,反转整个序列应写成 sequence[::-1]。下面两种写法并不等价:
print(letters[::-1]) # GFEDCBA
print(letters[-1::-1]) # GFEDCBA
print(letters[-1:0:-1]) # GFEDCB,不包含索引 0 的 A
特别要注意:负步长时,省略的 stop 表示“第一个元素之前”;显式写 -1 则会先按负数索引解释成最后一个元素的位置。需要一直取到开头时,直接省略 stop 最清楚。
哪些对象支持切片
Python 的常见序列都支持切片,并通常返回与原对象相同类型的结果:
print([1, 2, 3][1:]) # [2, 3]
print((1, 2, 3)[1:]) # (2, 3)
print("Python"[1:]) # ython
print(b"Python"[1:]) # b'ython'
print(range(10)[2:8:2]) # range(2, 8, 2)
list、tuple、str、bytes、bytearray 和 range 都支持切片。其中,range 切片仍返回按需计算的 range,不会先生成全部整数。
字典、集合和普通迭代器没有位置概念,因此不支持切片:
values = iter([10, 20, 30])
# values[1:] # TypeError
需要对迭代器按位置截取时,可以使用 itertools.islice()。它不会一次复制全部数据,但只支持非负索引和正步长:
from itertools import islice
values = iter(range(100))
print(list(islice(values, 10, 20, 3))) # [10, 13, 16, 19]
切片会不会复制数据
对内置列表进行切片会创建一个新列表:
original = [1, 2, 3]
copied = original[:]
copied.append(4)
print(original) # [1, 2, 3]
print(copied) # [1, 2, 3, 4]
但这只是浅复制:新列表中的元素仍然引用原来的对象。
original = [[1], [2]]
copied = original[:]
copied[0].append(99)
print(original) # [[1, 99], [2]]
如果必须复制嵌套的可变对象,可以根据数据结构逐层创建新对象,或在确实需要时使用 copy.deepcopy()。
字符串和元组本身不可变,因此切片结果无法用来修改原对象。某些第三方类型可能返回共享原数据的视图,而不是副本;使用 NumPy、Pandas 等库时,应以该类型的文档为准。
使用切片修改列表
列表是可变序列,可以把一个可迭代对象赋给普通切片,一次替换多个元素:
numbers = [0, 1, 2, 3, 4]
numbers[1:4] = [10, 20]
print(numbers) # [0, 10, 20, 4]
当步长为 1 时,右侧元素数量可以与被替换部分不同。因此切片赋值也能插入或删除元素:
items = ["A", "D"]
items[1:1] = ["B", "C"] # 在索引 1 之前插入
print(items) # ['A', 'B', 'C', 'D']
items[1:3] = [] # 删除索引 1、2 的元素
print(items) # ['A', 'D']
右侧必须是可迭代对象。赋值字符串时会逐个插入字符,而不是把整个字符串当作一个元素:
letters = ["A", "D"]
letters[1:1] = "BC"
print(letters) # ['A', 'B', 'C', 'D']
# 如果要插入一个完整字符串,应再套一层列表
words = ["start", "end"]
words[1:1] = ["middle"]
给扩展切片赋值
step 不为 1 的切片称为扩展切片。给它赋值时,右侧元素数量必须与目标位置数量完全一致:
numbers = [0, 1, 2, 3, 4, 5]
numbers[::2] = [10, 20, 30]
print(numbers) # [10, 1, 20, 3, 30, 5]
# numbers[::2] = [10, 20] # ValueError,目标位置有 3 个
使用切片删除元素
del 可以按切片批量删除列表元素:
numbers = list(range(10))
del numbers[2:8:2]
print(numbers) # [0, 1, 3, 5, 7, 8, 9]
列表切片赋值和删除都会原地修改原列表。元组和字符串不可变,不支持这些操作。
slice 对象
方括号中的切片表达式本质上会创建一个 slice 对象。需要保存或复用相同规则时,可以显式调用 slice():
middle = slice(1, 5, 2)
print("ABCDEFG"[middle]) # BD
print([10, 20, 30, 40, 50][middle]) # [20, 40]
print(middle.start) # 1
print(middle.stop) # 5
print(middle.step) # 2
slice(stop)、slice(start, stop) 和 slice(start, stop, step) 分别对应切片中可写的一到三个参数。
slice.indices(length) 可以根据指定长度,把省略值、负数和越界值换算成可供 range() 使用的具体参数:
rule = slice(None, None, -1)
start, stop, step = rule.indices(5)
print((start, stop, step)) # (4, -1, -1)
print(list(range(start, stop, step))) # [4, 3, 2, 1, 0]
这个方法适合实现自定义序列或调试复杂切片。日常取值时,直接使用方括号语法更容易阅读。
常用写法
取得前后若干项
values = [10, 20, 30, 40, 50]
first_three = values[:3]
last_two = values[-2:]
without_last = values[:-1]
分割序列
cut = 2
left = values[:cut]
right = values[cut:]
print(left) # [10, 20]
print(right) # [30, 40, 50]
每隔若干项取一个
every_second = values[::2]
创建反向副本
reversed_values = values[::-1]
只需要从后向前遍历而不需要新列表时,使用 reversed() 更能表达意图,也避免复制整个列表:
for value in reversed(values):
print(value)
判断回文
def is_palindrome(text):
return text == text[::-1]
print(is_palindrome("level")) # True
这种写法适合中小型字符串。处理超大数据时,反向切片会额外创建副本,可以改为从两端逐项比较。
性能和选择
内置列表、元组和字符串切片通常需要复制结果中的元素或字符。若切片结果长度为 k,时间和额外空间通常都是 O(k)。
head = large_list[:100] # 复制前 100 个元素
all_items = large_list[:] # 复制整个列表
选择工具时可以遵循以下原则:
- 需要一个独立的小序列:使用普通切片;
- 只需反向遍历:使用
reversed(); - 处理迭代器或大量流式数据:使用
itertools.islice(); - 需要明确复制列表:
items.copy()比items[:]更直接; - 第三方数组是否复制数据:查阅对应库的视图与复制规则。
常见错误
把 stop 当成包含边界
numbers = [10, 20, 30, 40]
print(numbers[1:3]) # [20, 30],不包含索引 3
步长方向与边界顺序不一致
print(numbers[3:0]) # [],默认向右移动
print(numbers[3:0:-1]) # [40, 30, 20]
误以为切片是深复制
切片只复制外层序列。内层列表、字典或自定义对象仍可能被原序列和新序列共享。
为了反转而调用 reverse() 并接收返回值
numbers = [1, 2, 3]
result = numbers.reverse()
print(result) # None
print(numbers) # [3, 2, 1]
list.reverse() 原地修改列表并返回 None;numbers[::-1] 返回反向的新列表;reversed(numbers) 返回反向迭代器。三者应按是否需要修改、复制或只遍历来选择。
在 Unicode 文本中把“字符”当成视觉符号
Python 字符串切片按 Unicode 码点操作。某些带组合符号的文字或表情由多个码点组成,切片可能把用户眼中的一个完整符号拆开。普通中英文学习示例通常不受影响;处理用户可见字符边界时,应使用支持 Unicode 字素簇的专门工具。
练习
- 给定列表
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],分别取出前 3 项、后 3 项和所有偶数位置的元素。 - 用切片把字符串
"Python"反转,并解释为什么text[-1:0:-1]会少一个字符。 - 将列表
[1, 2, 3, 4, 5]中间的[2, 3, 4]替换成[20, 30]。 - 创建一个
slice对象,让它能从任意序列中取得索引1到7之间的奇数位置。 - 观察嵌套列表经过
[:]复制后修改内层元素的结果,并解释浅复制行为。
小结
- 切片格式为
sequence[start:stop:step],包含start,不包含stop。 - 正步长从左向右,负步长从右向左;边界顺序必须与方向一致。
- 省略边界时,Python 会根据步长方向从序列开头或末尾开始。
- 普通索引越界会报错,切片边界越界会自动收缩。
- 内置列表切片会创建浅复制;切片赋值和
del可以批量修改列表。 slice对象适合保存和复用规则,islice()适合按需截取迭代器。- 对内置序列切片通常需要
O(k)时间和额外空间,应根据数据规模选择切片、迭代器或视图。