跳到主要内容

切片

切片(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. 从索引 1 开始;
  2. 每次向右移动 3 个位置;
  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 字素簇的专门工具。

练习​

  1. 给定列表 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],分别取出前 3 项、后 3 项和所有偶数位置的元素。
  2. 用切片把字符串 "Python" 反转,并解释为什么 text[-1:0:-1] 会少一个字符。
  3. 将列表 [1, 2, 3, 4, 5] 中间的 [2, 3, 4] 替换成 [20, 30]。
  4. 创建一个 slice 对象,让它能从任意序列中取得索引 1 到 7 之间的奇数位置。
  5. 观察嵌套列表经过 [:] 复制后修改内层元素的结果,并解释浅复制行为。

小结​

  • 切片格式为 sequence[start:stop:step],包含 start,不包含 stop。
  • 正步长从左向右,负步长从右向左;边界顺序必须与方向一致。
  • 省略边界时,Python 会根据步长方向从序列开头或末尾开始。
  • 普通索引越界会报错,切片边界越界会自动收缩。
  • 内置列表切片会创建浅复制;切片赋值和 del 可以批量修改列表。
  • slice 对象适合保存和复用规则,islice() 适合按需截取迭代器。
  • 对内置序列切片通常需要 O(k) 时间和额外空间,应根据数据规模选择切片、迭代器或视图。