序列切片深入解析

切片(slicing)是 Python 里最”看起来简单,实际藏了很多细节”的语法。前面 列表元组字符串 都用过 a[1:3],但你可能不知道:切片有第三个参数(步长)、切片赋值可以改变长度、slice 对象是可以复用的、numpy 数组切片是视图而不是拷贝……这一篇要把切片的”心智模型”讲通,让你以后遇到任何切片问题都能自己推导出来。

一、切片的三段式

完整语法:

1
序列[start:stop:step]
  • start:起始索引(含),默认为 0step > 0)或 -1step < 0);
  • stop:结束索引(不含),默认为 len(seq)-len(seq)-1
  • step:步长,默认为 1

三个参数都可以省略,冒号不能省。

1
2
3
4
5
6
7
8
9
a = [10, 20, 30, 40, 50, 60, 70]

a[2:5] # [30, 40, 50] 索引 2 到 5(不含 5)
a[:3] # [10, 20, 30] 从头到 3
a[3:] # [40, 50, 60, 70] 从 3 到末尾
a[:] # 完整拷贝
a[::2] # [10, 30, 50, 70] 步长 2
a[1::2] # [20, 40, 60] 奇数索引
a[::-1] # 反转

二、切片的”半开区间”

Python 切片遵循数学的”半开区间” [start, stop)含 start,不含 stop

好处:

  • len(a[i:j]) == j - i(当区间合法时),很好算;
  • 拼接方便:a[:i] + a[i:] 就是原列表;
  • a[i:i] 就是空切片,长度 0。

三、负数索引与负步长

1
2
3
4
5
6
a = [10, 20, 30, 40, 50]

a[-1] # 50
a[-3:-1] # [30, 40]
a[::-1] # 反转
a[-1:-4:-1] # [50, 40, 30] 步长 -1,从右往左

规则:负数索引 = 长度 + 索引a[-1] 就是 a[len(a)-1]

负步长时:默认 start 是最后一个元素,stop 是”越过第一个的位置”。

四、越界不报错

切片的一个奇妙特性是从不越界报错

1
2
3
4
a = [1, 2, 3]
a[10] # ❌ IndexError
a[10:20] # ✅ 返回 []
a[-100:2] # ✅ 返回 [1, 2]

Python 会自动把越界的索引”夹紧”到合法范围。

五、切片赋值

列表(可变序列)支持切片赋值,这是切片最强大的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
a = [1, 2, 3, 4, 5]

# 长度相同:直接替换
a[1:3] = [20, 30] # [1, 20, 30, 4, 5]

# 长度不同:延伸/收缩
a[1:3] = [10, 20, 30] # [1, 10, 20, 30, 4, 5]
a[1:4] = [] # [1, 4, 5] 删除元素

# 用切片插入元素
a = [1, 2, 3]
a[1:1] = [10, 20] # [1, 10, 20, 2, 3]

# 步长赋值:长度必须严格相等
a = [1, 2, 3, 4, 5, 6]
a[::2] = [100, 200, 300] # [100, 2, 200, 4, 300, 6]
a[::2] = [100, 200] # ❌ ValueError: 长度不匹配

注意:字符串和元组是不可变的,不支持切片赋值。

六、切片是浅拷贝

b = a[:] 是列表的浅拷贝

1
2
3
4
a = [[1, 2], [3, 4]]
b = a[:]
b.append([5, 6]) # a 不变
b[0][0] = 999 # a[0] 也变!内层引用共享

要独立的深拷贝:copy.deepcopy(a)

七、slice 对象

[start:stop:step] 本质是 slice(start, stop, step) 对象:

1
2
3
s = slice(1, 5, 2)
a = [10, 20, 30, 40, 50, 60]
print(a[s]) # [20, 40] 等价于 a[1:5:2]

好处:

  • 复用切片规则s = slice(2, None) 起名 HEAD_TWO_SKIP,在多个数组里复用;
  • 传参:函数接收 slice 参数比接收 start/stop/step 三个参数干净;
  • NumPy 里切片对象大量使用。

八、字符串切片

字符串切片行为跟列表一致,但返回新字符串(因为不可变):

1
2
3
4
5
6
7
8
9
s = "abcdefg"

s[2:5] # 'cde'
s[::-1] # 'gfedcba' 反转
s[::2] # 'aceg'

# 判断回文
def is_palindrome(s: str) -> bool:
return s == s[::-1]

九、字节和 bytearray

1
2
3
4
5
6
b = b"hello"
print(b[1:4]) # b'ell'

ba = bytearray(b"hello")
ba[1:4] = b"XYZ"
print(ba) # bytearray(b'hXYZo')

bytes 是不可变的(像 str),bytearray 是可变的(像 list of bytes)。

十、多维切片(NumPy 预告)

Python 内置容器只支持一维切片。多维切片是 NumPy 的强项:

1
2
3
4
5
6
7
8
9
10
11
12
13
import numpy as np
arr = np.arange(20).reshape(4, 5)
# [[ 0 1 2 3 4]
# [ 5 6 7 8 9]
# [10 11 12 13 14]
# [15 16 17 18 19]]

arr[1:3, 2:4] # 取子矩阵
# [[ 7 8]
# [12 13]]

arr[:, 0] # 第一列
arr[::2, ::2] # 隔一行隔一列

注意:NumPy 切片返回视图(view)而不是拷贝,修改视图会修改原数组。这跟内置的 list/str/tuple 相反。

十一、切片的性能特性

  • 内置容器切片是 O(k),k 是切片长度(要复制数据);
  • 切片会分配新对象,频繁切片会增加内存压力;
  • 单纯”跳过前 n 个”若数据量大,考虑用 itertools.islice
1
2
3
4
5
from itertools import islice
# 逐条读 100 万行文件的第 100-200 行
with open("huge.log") as f:
for line in islice(f, 100, 200):
print(line)

islice 支持任何可迭代对象且懒惰,比先 list() 再切片省内存。

十二、切片常用套路

1. 反转

1
2
s[::-1]              # 字符串/列表反转
list(reversed(s)) # 等价但返回列表

2. 拷贝

1
2
b = a[:]
# 更清晰:b = list(a) 或 a.copy()

3. 分块

1
2
3
4
5
6
def chunks(lst, n):
"""把 lst 按每 n 个一组切分。"""
for i in range(0, len(lst), n):
yield lst[i:i+n]

list(chunks([1,2,3,4,5,6,7], 3)) # [[1,2,3], [4,5,6], [7]]

4. 弹出多个

1
2
head, *rest = a          # 用解包
head, tail = a[0], a[1:] # 用切片

5. 移除头/尾

1
2
3
a = a[1:]        # 去掉第一个
a = a[:-1] # 去掉最后一个
a = a[1:-1] # 去掉首尾

6. 保留最后 n 个

1
last_n = a[-n:]

7. 交换两段

1
2
mid = len(a) // 2
a = a[mid:] + a[:mid] # 前后半段交换

十三、切片与索引的边界

1
2
3
4
5
6
7
8
9
10
11
12
a = [1, 2, 3, 4, 5]

# 单索引
a[0] # 1
a[-1] # 5
a[10] # IndexError

# 切片
a[0:0] # []
a[10:20] # []
a[-10:10] # 完整列表
a[:0] # []

初学者常常混淆索引和切片的边界,简单的记忆:

  • 单索引:,越界报错;
  • 切片:start 含,stop 不含,越界不报错

十四、一个综合例子:滚动窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# rolling.py
"""对一个序列做定长滚动窗口,计算窗口内平均值。"""

from statistics import mean

def rolling_avg(data: list[float], window: int) -> list[float]:
"""每个窗口的平均值。data 长度 n → 输出长度 n-window+1。"""
if window > len(data):
return []
return [mean(data[i:i+window]) for i in range(len(data) - window + 1)]


def paginate(lst: list, page_size: int) -> list[list]:
"""按 page_size 分页。"""
return [lst[i:i+page_size] for i in range(0, len(lst), page_size)]


def head_tail(lst: list, n: int) -> tuple[list, list]:
"""返回头 n 个和尾 n 个(不重叠)。"""
if len(lst) < 2 * n:
return lst, []
return lst[:n], lst[-n:]


if __name__ == "__main__":
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print("滚动平均 (window=3):", rolling_avg(data, 3))
print("分页 (size=4):", paginate(data, 4))
print("头尾 3 个:", head_tail(data, 3))

十五、常见陷阱

陷阱 1:s[::-1] 反转对 dict/set 无效

只有序列(list/tuple/str/bytes/range 等)支持切片。dict 和 set 是无序的,不能切片。

1
{1, 2, 3}[::-1]     # ❌ TypeError

陷阱 2:切片赋值时步长冲突

1
2
3
a = [1, 2, 3, 4]
a[::2] = [10, 20, 30] # ❌ 步长切片必须长度严格匹配
a[::2] = [10, 20] # ✅

陷阱 3:a[:] = ba = b 不同

1
2
3
4
5
6
7
8
9
a = [1, 2, 3]
c = a
a[:] = [10, 20] # 修改原对象,c 也变
print(c) # [10, 20]

a = [1, 2, 3]
c = a
a = [10, 20] # a 指向新对象,c 不变
print(c) # [1, 2, 3]

a[:] = b 是”就地替换 a 的全部元素”,a = b 是”让 a 名字指向新对象”。

陷阱 4:字符串切片”高开销”

Python 字符串切片会复制内存。逐字符处理超大字符串时,切片可能比想象中慢。

陷阱 5:NumPy 切片是视图,Python list 是拷贝

1
2
3
4
5
lst = [1, 2, 3]
lst[1:2][0] = 999 # 什么都没发生,lst 不变

arr = np.array([1, 2, 3])
arr[1:2][0] = 999 # arr[1] 变成 999!

十六、小结与延伸阅读

  • 切片语法 [start:stop:step],含头不含尾;
  • 越界不报错,索引越界才报错;
  • 负数索引 = 长度 + 索引;
  • 切片是浅拷贝,嵌套对象共享;
  • 列表支持切片赋值,可变增/删;
  • slice(start, stop, step) 是可复用的切片对象;
  • 反转 s[::-1]、拷贝 s[:]、分块用切片;
  • 大数据流用 itertools.islice
  • NumPy 切片是视图,行为不同。

延伸阅读:

下一篇 常用内置函数汇总 我们把 Python 内置的 70+ 个函数一次梳理清楚。