Python 切片怎么用?切片会不会产生新对象?
简化版
切片语法是 seq[start:stop:step],表示从 start 开始,到 stop 之前结束,按 step 步长取元素。对 list、tuple、str 这类序列做普通切片通常会产生新对象,但新容器里的元素引用仍可能和原容器共享。
详细版
基本语法:
nums = [0, 1, 2, 3, 4, 5]
print(nums[1:4]) # [1, 2, 3]
print(nums[:3]) # [0, 1, 2]
print(nums[3:]) # [3, 4, 5]
print(nums[::2]) # [0, 2, 4]
print(nums[::-1]) # [5, 4, 3, 2, 1, 0]
关键点:
start默认是开头;stop默认是结尾;step默认是 1;stop不包含;- 负数下标从末尾开始;
[::-1]常用于反转序列。
切片会创建新容器:
a = [1, 2, 3]
b = a[:]
print(a is b) # False
但如果元素本身是可变对象,内部对象仍然共享:
a = [[1], [2]]
b = a[:]
b[0].append(9)
print(a) # [[1, 9], [2]]
完整版教学
一、切片的区间是左闭右开
seq[start:stop] 取的是:
start <= index < stop
例如:
s = "abcdef"
print(s[1:4]) # bcd
包含下标 1、2、3,不包含 4。左闭右开的好处是:
s[:i]和s[i:]正好能无重叠拼回原序列;- 切片长度容易计算:
stop - start; - 边界更统一。
数字例子:s = "abcdef",s[1:4] 的长度是 4 - 1 = 3,结果正好是下标 1、2、3 的 "bcd"。s[:3] + s[3:] 会得到原字符串,因为前者取 0、1、2,后者从 3 开始,没有重叠也没有空洞。
二、省略 start、stop、step 的含义
nums = [0, 1, 2, 3, 4]
nums[:3] # 从开头到 3 之前
nums[2:] # 从 2 到结尾
nums[:] # 复制整个序列
nums[::2] # 每隔一个取一个
nums[::-1] # 反向复制
step 可以为负数。负数步长表示反向走:
print(nums[4:1:-1]) # [4, 3, 2]
这里从下标 4 开始,反向走到下标 1 之后停止,不包含下标 1。
可以把 step 理解成“游标每次走几格”。正数从左往右,负数从右往左;step 不能为 0,因为游标不前进就无法遍历。负步长时,默认起点会变成末尾附近,默认终点会越过开头,所以 [::-1] 能反转整个序列。
nums = [0, 1, 2, 3, 4]
index 0 1 2 3 4
nums[4:1:-1] 走 4 -> 3 -> 2,停在 1 之前
三、切片会创建新容器,但通常是浅拷贝
切片通常返回同类序列:
print("abc"[1:]) # "bc"
print((1, 2, 3)[1:]) # (2, 3)
print([1, 2, 3][1:]) # [2, 3]
但要注意,切片复制的是容器结构,不一定深度复制元素对象。对 list 来说,a[:] 会创建一个新列表,外层列表身份不同;里面的元素引用会被拷过去,如果元素本身是可变对象,内部仍然共享。
users = [{"name": "Tom"}, {"name": "Jerry"}]
new_users = users[:]
new_users[0]["age"] = 18
print(users[0]) # {'name': 'Tom', 'age': 18}
这是浅拷贝效果:外层列表是新的,里面的字典还是同一个。
users ──> [ ref0, ref1 ]
| |
new_users ──> [ ref0, ref1 ]
v v
dict0 dict1
所以 a[:] 常用于复制外层列表,但不能替代 copy.deepcopy()。字符串和 tuple 的切片也会返回序列对象;由于它们不可变,解释器在某些完整切片场景下可能复用对象,但语义上你不应该依赖身份,只关心值即可。
四、切片赋值是 list 的强大特性
列表支持切片赋值:
nums = [1, 2, 3, 4]
nums[1:3] = [20, 30, 40]
print(nums) # [1, 20, 30, 40, 4]
也可以删除一段:
nums = [1, 2, 3, 4]
nums[1:3] = []
print(nums) # [1, 4]
字符串和 tuple 不支持这种原地修改,因为它们不可变。
切片赋值的右侧长度不必和左侧一样,这使它可以完成插入、替换、删除。比如 nums[1:1] = [9, 9] 表示在下标 1 前插入两个元素;nums[::2] = ... 这类带步长的扩展切片赋值则要求长度匹配,否则会报错,因为它是在替换固定位置。
| 操作 | 示例 | 含义 |
|---|---|---|
| 替换 | a[1:3] = [8, 9] | 替换一段 |
| 插入 | a[1:1] = [8, 9] | 在位置 1 插入 |
| 删除 | a[1:3] = [] | 删除一段 |
| 反转副本 | a[::-1] | 生成反向新列表 |
五、越界和负下标怎么理解
易错点一:切片越界不会像普通下标那样报错。
nums = [1, 2, 3]
print(nums[:100]) # [1, 2, 3]
但普通下标越界会报错:
# nums[100] # IndexError
这是因为切片会把越界范围裁剪到合法区间。nums[:100] 等价于“从开头尽量取到第 100 个之前”,列表只有 3 个元素,就返回全部。普通下标 nums[100] 要求第 100 个位置真实存在,不存在就报错。
负下标也只是从末尾换算位置:长度为 6 的序列里,-1 是下标 5,-2 是下标 4。s[-4:-1] 对 "abcdef" 来说就是下标 2、3、4,结果为 "cde"。负数和左闭右开并不冲突,只是先换算大致位置,再按切片规则取。
六、性能和使用边界
切片很方便,但它会创建新序列或修改原列表,因此要考虑数据规模。big_list[::-1] 会创建一个反向新列表,如果原列表有 100 万个元素,新列表也要保存 100 万个引用。只想反向迭代时,可以用 reversed(big_list),它返回迭代器,不会立刻复制整个列表。
for x in reversed(big_list):
...
同理,seq[a:b] 在循环里频繁切大段数据,可能造成大量临时对象。算法题中切片写法简洁,但如果追求最优空间复杂度,要看是否可以用下标边界代替复制子序列。
易错点:
a[:]复制的是外层容器,不是深拷贝;[::-1]生成反向结果,也不是零成本视图。
七、常见误区与追问
- 误区:切片的
stop会被包含。 Python 切片是左闭右开,stop不包含,长度更容易计算。 - 误区:
a[:]是深拷贝。 它通常只复制外层容器,内部可变对象仍可能共享。 - 误区:切片越界一定报错。 切片会裁剪到合法范围,普通下标越界才会
IndexError。 - 追问:
[::-1]和reversed()有什么区别? 前者创建反向新序列,后者返回反向迭代器,更省内存。 - 追问:切片赋值为什么能改变列表长度? 普通切片替换的是一个区间,右侧序列长度可以不同,所以能插入或删除。
- 追问:负步长时为什么
nums[4:1:-1]不含下标 1? 左闭右开规则仍成立,只是方向反过来,走到 stop 之前停止。
八、加强记忆
把切片记成“三个旋钮”:start 控起点,stop 控终点但不包含,step 控方向和步长。普通切片会生成新序列或新外层容器,列表切片赋值能原地替换区间;遇到负下标、反转和越界时,先画下标轴,再按左闭右开和步长方向走。