
前言索引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.8s Pythonprint(s[0]) # Pprint(s[5]) # nprint(s[-1]) # nprint(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时取0j省略或为None时取len(s)。i或j为负时加上len(s)。规整后小于0的按0处理大于len(s)的按len(s)处理。结果包含下标k条件是i k j。若i j结果为空。# 适用于 Python 3.8s Pythonprint(s[1:4]) # yth 下标 1、2、3print(s[-3:-1]) # ho 等价于 s[3:5]print(s[1:100]) # ython 100 被夹成 6print(s[-100:2]) # Py -100 被夹成 0print(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.8s Pythonsl slice(1, 4)print(s[sl]) # yth与 s[1:4] 等价print(sl.start, sl.stop) # 1 4print(sl.step) # None需要把一段范围当作数据传来传去时slice对象比分别传start、stop更整洁比如自定义类实现__getitem__时就能直接收到它。四、理解负数步长带步长的切片s[i:j:k]取的是下标序列i, ik, i2k, ...直到到达j不含j。关键是步长的正负会改变i、j的默认值步长i默认j默认正k 00从头len(s)到尾负k 0len(s) - 1从尾开头之前即到下标 0 为止# 适用于 Python 3.8s Pythonprint(s[::2]) # Pto 下标 0、2、4print(s[1::2]) # yhn 下标 1、3、5print(s[::-1]) # nohtyP 整串反转print(s[4:1:-1]) # oht 下标 4、3、2print(s[1::-1]) # yP 下标 1、0有了这张表s[::-1]为什么能反转就清楚了负步长让起点默认在末尾、终点默认在开头之前于是从后往前逐个取。而s[4:1:-1]手动指定了i4、j1取下标 4、3、2到 1 停不含 1。k不能为0否则抛ValueError。五、切片赋值只对可变序列成立对str、tuple这类不可变序列切片只能读对list这类可变序列切片还能写。# 适用于 Python 3.8lst [0, 1, 2, 3, 4, 5]lst[2:4] [a, b, c] # 右侧个数不必等于左侧长度print(lst) # [0, 1, a, b, c, 4, 5]但str不行# 适用于 Python 3.8s 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..12def 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, deptprint(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✅ 负步长要取到开头终点要省略或写Nones[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不含stopstart 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 日停止维护这些旧写法不要再用了。