Python 切片:一行代码搞定"截取"的瑞士军刀
引言:切蛋糕的艺术
想象你面前有一条长长的瑞士卷蛋糕,你想:
- 切最前面的 3 块给客人;
- 切最后面的 2 块自己留着;
- 每隔 1 块切 1 块,尝尝不同位置的味道;
- 或者干脆切一条"从中间到结尾"的大块。
如果每次都要一块一块地数着切,那太麻烦了。你只需要告诉蛋糕师傅:"从第几块开始,到第几块结束,每隔几块切一块",师傅一刀下去,完事。
Python 的**切片(Slice)**就是这个"蛋糕师傅"。它用一行简洁的代码,就能从列表、元组、字符串中精确截取出你想要的任意部分。这是 Python 最常用、最优雅的特性之一,掌握它,你的代码会立刻简洁一倍。
一、为什么需要切片?从"笨办法"说起
1.1 问题的出现
假设有一个班级名单:
L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack']老师要叫前 3 个同学去办公室,怎么取出来?
笨办法 1:一个一个拿
>>> [L[0], L[1], L[2]]
['Michael', 'Sarah', 'Tracy']问题:如果要取前 30 个呢?难道写 L[0], L[1], ..., L[29]?手指会断的。
笨办法 2:用循环
>>> r = []
>>> n = 3
>>> for i in range(n):
... r.append(L[i])
...
>>> r
['Michael', 'Sarah', 'Tracy']可以是可以,但 4 行代码干一件小事,太啰嗦了。
1.2 切片的解决方案
>>> L[0:3]
['Michael', 'Sarah', 'Tracy']一行搞定。生活化理解:就像对蛋糕师傅说"给我第 0 块到第 3 块之间的蛋糕"(注意:不包含第 3 块),师傅手起刀落,正好 3 块。
这就是切片的核心语法:序列[开始索引:结束索引],取从开始到结束、但不包含结束的那一段。
二、切片语法详解:三个参数的魔法
切片的完整语法是:
序列[开始:结束:步长]三个参数都可以省略,非常灵活。我们逐个拆解。
2.1 开始和结束:左闭右开区间
这是切片最容易犯错的地方,记住一句口诀:"含头不含尾"(左闭右开)。
L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack']
# 索引: 0 1 2 3 4| 切片写法 | 含义 | 结果 |
|---|---|---|
L[0:3] | 从索引 0 到 3(不含 3) | ['Michael', 'Sarah', 'Tracy'] |
L[1:3] | 从索引 1 到 3(不含 3) | ['Sarah', 'Tracy'] |
L[:3] | 开头省略 = 从 0 开始 | ['Michael', 'Sarah', 'Tracy'] |
L[2:] | 结尾省略 = 直到最后 | ['Tracy', 'Bob', 'Jack'] |
生活化理解:"含头不含尾"就像电影院座位号"从 5 排 1 座到 5 排 10 座"通常意味着 1~9 座,因为 10 座是下一段的开始。这种设计让 L[0:n] 正好取 n 个元素,L[a:b] 的长度正好是 b - a,非常整齐。
2.2 负数索引:从尾巴开始数
Python 支持负数索引:-1 表示倒数第一个,-2 表示倒数第二个。切片同样支持:
>>> L[-2:] # 从倒数第 2 个到最后
['Bob', 'Jack']
>>> L[-2:-1] # 从倒数第 2 个到倒数第 1 个(不含)
['Bob']
>>> L[:-1] # 从开头到倒数第 1 个(不含),即"去掉最后一个"
['Michael', 'Sarah', 'Tracy', 'Bob']记忆技巧:想象一排人,正数是"从前往后数第几个",负数是"从后往前数第几个"。-1 永远是队尾那个人。
2.3 步长:跳着取
第三个参数是步长,表示"每隔几个取一个":
>>> L = list(range(10)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> L[:6:2] # 前 6 个里,每隔 2 个取 1 个
[0, 2, 4]
>>> L[::2] # 整个列表,每隔 2 个取 1 个
[0, 2, 4, 6, 8]
>>> L[1::2] # 从索引 1 开始,每隔 2 个取 1 个
[1, 3, 5, 7, 9]生活化理解:步长就像做广播体操时"第 1、3、5、7 排同学出列"——不是连着叫,而是跳着叫。
负数步长:步长还可以是负数,表示从右往左取:
>>> L[::-1] # 步长 -1,整个列表倒序
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
>>> L[5:1:-1] # 从索引 5 倒着取到索引 1(不含 1)
[5, 4, 3, 2][::-1] 是反转列表/字符串的经典写法,面试常考。
2.4 什么都不写:完整复制
>>> L[:]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9][:] 会原样复制一份列表。注意这是浅拷贝——新列表和旧列表是两个独立的对象,修改互不影响(但如果元素本身是可变对象,比如列表里套列表,内部还是共享的)。
>>> a = [1, 2, 3]
>>> b = a[:] # 复制一份
>>> b.append(4)
>>> a
[1, 2, 3] # 原列表不受影响三、切片的适用对象:不只是列表
切片不是列表的专利,Python 中所有"序列类型"都支持切片。
3.1 元组(tuple):切片结果还是元组
>>> (0, 1, 2, 3, 4, 5)[:3]
(0, 1, 2)
>>> ('a', 'b', 'c', 'd')[::2]
('a', 'c')元组不可修改,但切片读取完全没问题。
3.2 字符串:每个字符就是一个元素
字符串可以看成"字符组成的列表",切片极其常用:
>>> 'ABCDEFG'[:3]
'ABC'
>>> 'ABCDEFG'[::2]
'ACEG'
>>> 'hello world'[::-1]
'dlrow olleh'
>>> phone = '13812345678'
>>> phone[:3] + '****' + phone[-4:] # 手机号打码
'138****5678'背景知识:很多编程语言为字符串提供了各种截取函数(如 Java 的 substring()、JavaScript 的 substr()),而 Python 没有单独的字符串截取函数——因为切片一个操作就全搞定了。这是 Python 设计哲学"用一种方法做好一件事"的体现。
3.3 range、bytes 等其他序列
>>> range(100)[10:20:3]
range(10, 20, 3)
>>> b'ABCDEF'[2:5]
b'CDE'四、实战演练:0-99 数列的花式切法
用一个 100 个数字的列表,把所有切片技巧过一遍:
>>> L = list(range(100))| 需求 | 切片写法 | 结果 |
|---|---|---|
| 前 10 个数 | L[:10] | [0, 1, ..., 9] |
| 后 10 个数 | L[-10:] | [90, 91, ..., 99] |
| 第 11~20 个数 | L[10:20] | [10, 11, ..., 19] |
| 前 10 个数里每隔 2 个取 1 个 | L[:10:2] | [0, 2, 4, 6, 8] |
| 所有数每隔 5 个取 1 个 | L::5 或 L[::5] | [0, 5, 10, ..., 95] |
| 整个列表倒序 | L[::-1] | [99, 98, ..., 0] |
| 去掉首尾各一个 | L[1:-1] | [1, 2, ..., 98] |
实战技巧总结:
- 取前 n 个:
L[:n] - 取后 n 个:
L[-n:] - 去掉前 n 个:
L[n:] - 去掉后 n 个:
L[:-n] - 反转:
L[::-1]
五、真实应用场景
5.1 分页显示
网页上"每页显示 10 条"的分页功能,本质就是切片:
def get_page(items, page, page_size=10):
"""获取第 page 页的数据(页码从 1 开始)"""
start = (page - 1) * page_size
end = start + page_size
return items[start:end]
articles = list(range(1, 96)) # 假设有 95 篇文章
print(get_page(articles, 1)) # 第 1 页:[1..10]
print(get_page(articles, 10)) # 第 10 页:[91..95],不够 10 条也正常返回注意:切片越界不会报错!articles[90:100] 不会抛异常,而是自动取到最后一个为止。这是切片比直接索引 L[95] 更安全的地方。
5.2 字符串处理:文件扩展名
filename = 'report.final.v2.pdf'
# 取扩展名(最后一个 . 之后的部分)
ext = filename[filename.rfind('.')+1:] # 'pdf'
# 取文件名主体(最后一个 . 之前的部分)
name = filename[:filename.rfind('.')] # 'report.final.v2'5.3 数据抽样
从大量数据中均匀抽样:
data = list(range(10000))
sample = data[::100] # 每隔 100 个取 1 个,共 100 个样本5.4 队列/栈操作
queue = [1, 2, 3, 4, 5]
first = queue[0] # 查看队首
queue = queue[1:] # 出队:去掉第一个元素(简单场景可用,大数据量建议用 collections.deque)六、经典练习:手写 trim() 函数
题目:利用切片实现一个 trim(s) 函数,去除字符串首尾的空格,不允许使用字符串自带的 strip() 方法。
def trim(s):
# 找到左边第一个非空格字符的位置
start = 0
while start < len(s) and s[start] == ' ':
start += 1
# 找到右边第一个非空格字符的位置
end = len(s)
while end > start and s[end - 1] == ' ':
end -= 1
return s[start:end]
# 测试
if trim(' hello ') != 'hello':
print('测试失败!')
elif trim(' hello') != 'hello':
print('测试失败!')
elif trim('hello ') != 'hello':
print('测试失败!')
elif trim(' hello world ') != 'hello world':
print('测试失败!')
elif trim('') != '':
print('测试失败!')
elif trim(' ') != '':
print('测试失败!')
else:
print('测试成功!')思路解析:
- 用两个指针
start和end分别从头、从尾向中间收缩,跳过空格; - 收缩完毕后,中间剩下的部分就是去空格后的内容;
- 最后用切片
s[start:end]一刀取出。
这正好演示了切片的精髓:先算出边界,再一刀取出,而不是一个字符一个字符地搬。
七、常见误区与避坑指南
误区 1:以为 L[0:3] 包含索引 3
L = ['a', 'b', 'c', 'd', 'e']
L[0:3] # 是 ['a', 'b', 'c'],不包含 'd'!牢记含头不含尾(左闭右开)。判断长度的小技巧:L[a:b] 的长度 = b - a。
误区 2:混淆切片和索引的越界行为
L = [1, 2, 3]
L[10] # 报错!IndexError: list index out of range
L[10:20] # 不报错,返回 []
L[1:100] # 不报错,返回 [2, 3]切片越界宽容,索引越界严格。这在写防御性代码时很有用——不用担心切片会抛异常。
误区 3:以为 b = a[:] 是深拷贝
a = [[1, 2], [3, 4]]
b = a[:] # 浅拷贝
b[0].append(99)
print(a) # [[1, 2, 99], [3, 4]] 原列表也被改了![:] 只复制了外层,内层的子列表还是共享的。需要完全独立时请用 copy.deepcopy()。
误区 4:负数步长时搞反开始和结束
L = [0, 1, 2, 3, 4, 5]
L[1:5:-1] # 结果:[] 空的!
L[5:1:-1] # 结果:[5, 4, 3, 2] 正确规律:步长为负时,"开始索引"必须大于"结束索引"(因为是从右往左走)。可以记成:步长为负,起点在右,终点在左。
误区 5:对字符串用切片后试图修改
s = 'hello'
s[0] = 'H' # 报错!字符串不可变
s = 'H' + s[1:] # 正确:用切片+拼接生成新字符串字符串和元组不可变,切片只能用于读取,不能用于赋值。列表则可以:
L = [1, 2, 3, 4, 5]
L[1:3] = ['a', 'b', 'c'] # 列表切片可以赋值,甚至可以改变长度
print(L) # [1, 'a', 'b', 'c', 4, 5]误区 6:步长为 0
L[::0] # 报错!ValueError: slice step cannot be zero步长表示"每隔几个取一个",取 0 个没有意义,Python 直接拒绝。
八、动手练习
- 基础题:给定
s = 'abcdefghij',用切片分别取出'cde'、'jih'(倒序)、'bdfhj'(偶数位置字符)。 - 进阶题:编写函数
is_palindrome(s),用一行切片代码判断字符串是否为回文(正读反读都一样,如'level')。 - 挑战题:给定列表
L = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],不借助任何新变量,只用切片操作,把列表变成[6, 7, 8, 9, 10, 1, 2, 3, 4, 5](即前后两半互换)。 - 思考题:
L[:]和list(L)都能复制列表,它们有区别吗?在什么场景下会有不同表现?(提示:想想元组)
小结
- 切片语法:
序列[开始:结束:步长],取"含头不含尾"的一段;三个参数都可省略,默认值是"从头、到尾、步长 1"。 - 负数索引:
-1是最后一个元素,支持倒数切片,如L[-2:]。 - 负数步长:
[::-1]一键反转序列,是最经典的切片技巧。 - 适用范围:列表、元组、字符串等所有序列类型都支持切片;Python 不需要单独的字符串截取函数。
- 越界安全:切片越界不报错,自动截断;直接索引越界会抛
IndexError。 - 核心心法:切片把"循环逐个取元素"简化成了"算好边界,一刀取出",让代码更短、更清晰、更不易出错。