前言
索引(indexing)和切片(slicing)是 Python 序列的两把基础钥匙,但很多人是"用得会、说不出"。一旦遇到边界情况——为什么s[100]报错而s[100:200]不报错?为什么s[::-1]能反转?s[-0]到底是什么——就只能靠试。其实它们背后有几条很一致的定义,理顺之后,边界情况不用记也能推出来。
要澄清一个流传很广的误解:"切片是索引的批量版"。这个说法不太准确。索引和切片是两套不同的规则:索引要求下标必须落在[0, len)之内,越界是错误;切片则先把下标规整到合法范围,再按"左闭右开"取一段,越界被容忍。正因为规则不同,才有"索引报错、切片不报错"这个看似矛盾的现象。
本文用一个统一的心智模型把两者串起来,配上一张下标对照图,最后归纳成可以现场推导的几条规则。代码基于 Python 3.8 及以上。Python 2.7 已于 2020 年 1 月 1 日停止维护,本文不使用 Python 2 的语法。
一、先建立一张下标图
拿s = "Python"举例,把每个字符、正下标和负下标画在一起看:
字符 P y t h o n
正下标 0 1 2 3 4 5
负下标 -6 -5 -4 -3 -2 -1两条规律一眼可见:
- 正下标从
0开始,最大的合法下标是len(s) - 1,也就是5。 - 负下标从
-1开始,表示最后一个字符;最小的是-len(s),也就是-6。
负下标-k的值等于len(s) - k(当k在1..len范围内)。所以s[-1]和s[5]是同一个字符。
二、索引:必须落在合法范围
索引的规则只有一句:下标必须在[-len, len-1]内,否则抛IndexError。
# 适用于 Python 3.8+
s = "Python"
print(s[0]) # P
print(s[5]) # n
print(s[-1]) # n
print(s[-6]) # P
# print(s[6]) # IndexError: string index out of range
# print(s[-7]) # IndexError: string index out of range注意负下标下界是-len,-6合法,-7就越界了。
关于-0,要特别提一句:-0就是0,它不是"从末尾数的第 0 个"。s[-0]等于s[0],取到第一个字符。很多人以为s[-0]会像s[-1]那样指向末尾,这是个典型误解。
三、切片:先规整边界,再左闭右开
切片的规则稍多,但都是确定的。对s[i:j]:
i省略或为None时取0;j省略或为None时取len(s)。i或j为负时,加上len(s)。- 规整后,小于
0的按0处理,大于len(s)的按len(s)处理。 - 结果包含下标
k,条件是i <= k < j。若i >= j,结果为空。
# 适用于 Python 3.8+
s = "Python"
print(s[1:4]) # yth 下标 1、2、3
print(s[-3:-1]) # ho 等价于 s[3:5]
print(s[1:100]) # ython 100 被夹成 6
print(s[-100:2]) # Py -100 被夹成 0
print(s[4:1]) # '' 起点 >= 终点官方文档对这段规则的原文表述是:i或j小于-len(s)时用0,大于len(s)时用len(s);i大于等于j时切片为空。所以切片永远不会因为越界而报错,最多给你一个空串。
切片语法背后其实是一个内置对象slice。s[1:4]在求值时相当于构造slice(1, 4)再交给下标操作,两个参数时签名是slice(start, stop),三个参数是slice(start, stop, step):
# 适用于 Python 3.8+
s = "Python"
sl = slice(1, 4)
print(s[sl]) # yth,与 s[1:4] 等价
print(sl.start, sl.stop) # 1 4
print(sl.step) # None需要把"一段范围"当作数据传来传去时,slice对象比分别传start、stop更整洁,比如自定义类实现__getitem__时就能直接收到它。
四、理解负数步长
带步长的切片s[i:j:k],取的是下标序列i, i+k, i+2k, ...,直到到达j(不含j)。关键是步长的正负会改变i、j的默认值:
| 步长 | i默认 | j默认 |
|---|
正(k > 0) | 0(从头) | len(s)(到尾) |
负(k < 0) | len(s) - 1(从尾) | "开头之前",即到下标 0 为止 |
# 适用于 Python 3.8+
s = "Python"
print(s[::2]) # Pto 下标 0、2、4
print(s[1::2]) # yhn 下标 1、3、5
print(s[::-1]) # nohtyP 整串反转
print(s[4:1:-1]) # oht 下标 4、3、2
print(s[1::-1]) # yP 下标 1、0有了这张表,s[::-1]为什么能反转就清楚了:负步长让起点默认在末尾、终点默认在开头之前,于是从后往前逐个取。而s[4:1:-1]手动指定了i=4、j=1,取下标 4、3、2(到 1 停,不含 1)。
k不能为0,否则抛ValueError。
五、切片赋值:只对可变序列成立
对str、tuple这类不可变序列,切片只能读;对list这类可变序列,切片还能写。
# 适用于 Python 3.8+
lst = [0, 1, 2, 3, 4, 5]
lst[2:4] = ["a", "b", "c"] # 右侧个数不必等于左侧长度
print(lst) # [0, 1, 'a', 'b', 'c', 4, 5]但str不行:
# 适用于 Python 3.8+
s = "Python"
# s[0:2] = "Ja" # TypeError: 'str' object does not support item assignment原因还是那句:str不可变。想"改"字符串,只能拼出新串。
实战:用同一套规则解析定长字段
定长格式的记录(每段占固定字符数)是索引和切片的经典用武之地。
# 适用于 Python 3.8+
# 假设每行定长:姓名 6 字符、年龄 3 字符、部门 4 字符,共 13 字符
LINE = "Alice 025研发 "
# 0..5 6..8 9..12
def parse_fixed(line):
line = line.ljust(13) # 不足补齐,避免切片拿短
name = line[0:6].strip()
age = line[6:9].strip()
dept = line[9:13].strip()
return name, age, dept
print(parse_fixed(LINE)) # ('Alice', '025', '研发')这里每个字段的起点是"上一个字段的终点",正因为切片左闭右开,[0:6]和[6:9]首尾相接、不重不漏。如果切片是"左闭右闭",每一段都得写成[起点:终点-1],很容易差一。
常见坑点
- 混淆索引与切片的容错性
❌ 以为s[len(s)]会像切片那样安静返回空
✅s[len(s)]抛IndexError;只有s[len(s):]这类切片才返回空串
- 把
s[-0]当成末尾
❌s = "abc"; print(s[-0])期待c
✅-0就是0,输出a;要末尾用s[-1]
- 弄错左闭右开
❌"Python"[0:2]以为能取到Pyt
✅ 它只取下标 0、1,得到Py;要含下标 2 得写[0:3]
- 负数步长下起点终点想当然
❌s = "abcdef"; s[3:0:-1]以为能取全到开头,实际只得到下标 3、2、1
✅ 负步长要取到开头,终点要省略或写None:s[3::-1]得到dcba
- 用
find结果当索引却不判 -1
❌i = s.find(":"); s[i + 1:],找不到时-1 + 1 = 0,返回整串
✅ 先判断i != -1,或改用partition并检查分隔符非空
- 拿切片去做深拷贝
❌ 嵌套列表b = a[:]后改b[0][0],a也跟着变
✅ 切片是浅拷贝;深拷贝要copy.deepcopy(a)
- 步长写 0
❌s[::0]→ValueError: slice step cannot be zero
✅ 步长不允许为 0;取全部就省略步长
- 对字符串用切片赋值
❌s[1:2] = "X"→TypeError
✅ 只有可变序列支持切片赋值;字符串要拼出新串
总结
| 问题 | 规则 |
|---|
| 索引合法范围 | -len到len-1,越界抛IndexError |
| 切片越界 | 下标夹到[0, len],不报错 |
| 左闭右开 | 含start,不含stop;start >= stop得空串 |
-0 | 等于0,不是末尾 |
| 正步长默认 | 从0到len(s) |
| 负步长默认 | 从len-1到"开头之前" |
| 切片赋值 | 只有可变序列支持,str不支持 |
索引和切片不是同一件事的两种写法,而是两套定位规则。索引像"精确取一件",要求坐标必须有效;切片像"划一段范围",坐标超出会被温和地收进边界。把"左闭右开"和"负步长改变默认方向"这两条吃透,s[::-1]反转、s[1:100]不报错、s[-0]取到首字符这些现象就都能当场推出来,而不是靠背例子。
最后提一句版本:本文的写法面向 Python 3。Python 2 里print是语句而不是函数(3 里必须写print(...)),xrange已移除(3 里range本身就是惰性的),unicode类型统一成了str,整数除法也从3 / 2 == 1变成了3 / 2 == 1.5(整除要写//)。Python 2.7 已于 2020 年 1 月 1 日停止维护,这些旧写法不要再用了。