AI 技术博客
返回首页
Python 基础 16 分钟阅读

切片详解:步长、负索引与边界

Python切片字符串序列操作

引言

进阶字符串的第二站是切片(slicing)。第二章只给了入门三句话:下标从 0 开始、切片含头不含尾、越界静默截断;本篇把完整语法 s[start:stop:step] 一次讲透,包括负索引的换算、省略边界时的缺省规则、负步长的「倒着走」行为,以及 slice 对象——Python 内部切片机制的真实形态。

切片是序列(sequence)协议的通用操作,字符串、列表、元组全都支持,行为完全一致,只是返回值类型不同(字符串切片仍是字符串)。本篇以字符串为主,这正是为了给第六章的列表切片打好底子:列表多一个「切片赋值」(s[1:3] = […])的能力,本篇会作为对照点讲清楚。后面三篇(正则)与第八篇(nginx 日志解析实战)都会频繁使用切片截取字段,现在把边界规则练扎实,到时候才不会晕头转向。

概念与原理

切片是区间投影,不是「位置抄写」

s[a:b:c] 的语义是:从下标 a 出发,以 c 为步长前进,取所有「没越过 b 边界」的下标。b 是开区间边界——stop 位置的字符永远取不到,这就是「含头不含尾」。用数学语言说,切片是在区间 [a, b) 上做了一次等差采样:

step > 0 时取:a, a+c, a+2c, ...   直到 < b 的最大下标
step < 0 时取:a, a-c, a-2c, ...   直到 > b 的最小下标

与单索引不同,切片的越界是静默钳制的:超大的下标被压到串尾,过小的被抬到 0;而单索引越界直接抛 IndexError。这背后是设计取舍:单索引越界九成是 bug,应该尽早报错;切片越界截断则常见于「想截到结尾但没算准长度」的宽容场景,安静地返回能取到的部分更顺手。

负索引:从尾部数,等价于 len(s) + i

-1 是最后一个码点,-2 是倒数第二个。Python 在求值时把负索引换算成正索引再参与区间判断:s[-4:-1] 等价于 s[len(s)-4 : len(s)-1]。所以负索引与正索引可以混写在同一对边界里:s[2:-2] 是「掐头去尾各两个」的常用写法。

负步长:整段逻辑反转

步长为负时,方向反过来:从 start 出发递减,stop 仍是「不过界」的哨兵但不包含。缺省规则随之反转——start 缺省为 len(s)-1(串尾),stop 缺省为「走到索引 0 为止」(内部用 -1 作哨兵,用 slice.indices 可以验证这一点)。这是灵魂所在:s[::-1] 能反转整个字符串,而 s[:5:-1] 却常常是空串——start 缺省成串尾 5 号位,与 stop 重合,什么都取不到。易错点有专门一条讲它。

为什么切片返回「新串」而不是视图

str 不可变(第二章的底线),因此切片只能产生新字符串,底层是一次 O(k) 的复制(k 为切片长度),不会共享内存——这与 C 语言的「指针 + 长度」视图完全不同。细节提醒:CPython 对全切片 s[:] 与 s[::1] 有一条快速路径直接返回原对象(本机 3.12 实测 s is s[:] 为 True),但这是实现层面的优化,语言规范并不承诺,代码里不要依赖身份相等。列表切片返回新列表,元素是引用拷贝(浅拷贝),第六章展开。

复杂度与惯用恒等式

切片是一次 O(k) 的复制,k 是切片长度:s[2:5] 无论原串多长都只复制 3 个码点;而反转 s[::-1] 必须复制全部 n 个码点,是 O(n) 的整串拷贝。「在同一个大字符串的不同位置反复切片」是安全的(每次都是小复制),「循环里反复 s = s[::-1]」则是典型的热点代码。几条高频恒等式值得刻进肌肉记忆:s[::-1] 反转;s[::2] 与 s[1::2] 分奇偶位;s[:-1] 去掉最后一个码点(比如去掉行尾换行符);s[1:] 去掉第一个;而「掐头去尾各两个」就是 s[2:-2]。边界自查口诀只有一句:start 含、stop 不含、step 定方向。

slice 对象:切片的「图纸」

s[a:b:c] 是语法糖,运行时等价于 s[slice(a, b, c)]。slice 对象可以单独构造、保存、在多个序列上复用,还可以调用 .indices(长度) 算出「对长度为 n 的序列,实际取哪些下标」——返回三元组 (start, stop, step),是调试负步长边界的利器:slice(None, None, -1).indices(6) 返回 (5, -1, -1),其中 stop 的 -1 正是「走到 0 为止」的哨兵。

操作与实现

基础形态:start / stop 与负索引

s = "abcdefgh"
print(s[1:5])        # bcde:含头不含尾
print(s[:4])         # abcd:start 省略默认为 0
print(s[4:])         # efgh:stop 省略默认为 len(s)
print(s[-1])         # h:最后一个码点
print(s[-3:])        # fgh:倒数三个
print(s[-4:-1])      # efg:负边界区间
print(s[2:-2])       # cdef:正负混写
bcde
abcd
efgh
h
fgh
efg
cdef

s[-4:-1] 要按「先换算成正索引再判断」来理解:8 - 4 = 4 到 8 - 1 = 7,即下标 4 到 6。

常用写法速查

下面的表把最高频的切片写法固化下来(示例串 s = “abcdef”),每个结果都能用前面的区间规则推出来,反过来也适合自测:

写法结果典型用途
s[::-1]fedcba反转字符串、回文检测
s[::2]ace取偶数位、子采样
s[1::2]bdf取奇数位
s[-1]f取最后一个码点
s[:-1]abcde去掉末尾一个码点(如换行符)
s[1:]bcdef去掉开头一个码点
s[-3:]def取尾部定长内容(如扩展名)
s[2:] + s[:2]cdefab循环左移两位(拼接重建)

把这张表演算一遍再做练习,比对着报错调试快得多:写切片代码前先在大脑里过一遍「谁含谁不含、往哪个方向走」,边界几乎不会错。

步长:隔几个取一个

s = "0123456789"
print(s[::2])        # 02468:偶数位
print(s[1::2])       # 13579:奇数位
print(s[2:9:3])      # 258:从 2 起每 3 个取一个
print(s[8:0:-2])     # 8642:反向隔一个取一个
02468
13579
258
8642

步长为 2 的切片是「奇偶抽取」,在数据脱敏、抽样子采样里很常见;步长为 3 及以上则是按间隔抽样。s[8:0:-2] 从下标 8 出发倒着走,沿途取 8、6、4、2——注意 0 是开区间边界,取不到下标 0。

负步长与反转

s = "abcdef"
print(s[::-1])       # fedcba:反转,字符串处理最高频的切片之一
print(s[-1::-1])     # fedcba:显式写出起点,效果相同
print(s[4:1:-1])     # edc:倒着取一段
print(s[-1:0:-1])    # fedcb:stop 为 0,首字符取不到
print(s[::-2])       # fdb:反转后再隔一个取一个
print("上海自来水来自海上" == "上海自来水来自海上"[::-1])  # True
fedcba
fedcba
edc
fedcb
fdb
True

"上海自来水来自海上"[::-1] 是回文(palindrome)检测的标准写法:一个完整的 9 码点字符串反转后与自己相等(本机实测 len 为 9)。s[-1:0:-1] 的终点 0 是开区间——想连首字符一起取,就写 s[-1::-1] 或干脆 s[::-1]。

缺省与越界钳制

s = "abcdef"
print(s[3:99])        # def:stop 越界,钳制到 len(s)
print(s[-99:2])       # ab:start 越界,钳制到 0
print(repr(s[4:2]))   # '':正步长下起点在终点之后,得到空串
print(repr(s[1:1]))   # '':空区间合法
print(repr(s[6:]))    # '':起点在串尾之后也是空串
def
ab
''
''
''

这一组的对照是 print(s[99])——单索引越界直接抛 IndexError。把「报错」与「静默钳制」两套规则记成一对,遇到诡异空输出时先怀疑边界。

不可变与身份:为什么「修改一段」要重建

long = "hello" * 300
copy = long[:]
print(long is copy)      # True:CPython 全切片快速路径(实现细节,规范不保证)
rev = long[::-1]
print(long is rev)       # False:反转必然产生新对象
try:
    long[1:3] = "xx"     # str 不可变:没有切片赋值
except TypeError as err:
    print(err)
lst = [0, 1, 2, 3, 4]
lst[1:3] = [8, 9]        # 列表是可变序列:切片赋值合法
print(lst)
True
False
'str' object does not support item assignment
[0, 8, 9, 3, 4]

想「修改字符串的一段」,唯一正解是重建:s[:i] + 新内容 + s[i+1:],三步三个新字符串,这正是不可变对象代价的直观体现。

slice 对象:把切片参数存下来的正确方式

s = "abcdefgh"
part = slice(1, 5, 2)
print(s[part])                          # bd:等价于 s[1:5:2]
print(s[slice(2, -2)])                  # cdef:slice 里同样允许负边界
print(part.indices(len(s)))             # (1, 5, 2)
print(slice(None, 3).indices(len(s)))   # (0, 3, 1)
print(slice(None, None, -1).indices(len(s)))  # (7, -1, -1)
bd
cdef
(1, 5, 2)
(0, 3, 1)
(7, -1, -1)

indices 返回的是「规范化后的实际下标区间」:slice(None, None, -1).indices(8) 得到 (7, -1, -1),翻译过来就是「从 7 开始,每次减 1,直到越过 -1(0 已包含)」。构造一次 slice 对象,就能在多个序列、多个地方复用同一套切法。

工程合成:截取字段、脱敏与回文

stamp = "2026-09-01T10:30:05"
print(stamp[:10])            # 2026-09-01:取日期
print(stamp[11:])            # 10:30:05:取时间
filename = "report_2026.csv"
print(filename[-4:])         # .csv:取固定宽度扩展名
print(filename[:-4])         # report_2026:去掉扩展名
phone = "138-0013-8000"
print(phone[:3] + "****" + phone[-4:])   # 138****8000:手机号脱敏
price = "¥1280.50"
print(price[1:])             # 1280.50:去掉货币符号
sku = "SKU-2026-091"
print(sku[4:8])              # 2026:按固定段位截取
2026-09-01
10:30:05
.csv
report_2026
138****8000
1280.50
2026

sku[4:8] 这类「按固定段位截取」是解析定长报文的标准姿势:只要约定好每个字段的起止下标,一个切片就是一次字段提取。它与按分隔符切分(上一篇的 split)互补:结构既定就用切片,分隔符变化就用 split。

易错点与陷阱

陷阱一:步长为 0 直接报错

s[::0] 会抛 ValueError(slice step cannot be zero,本机 3.12 实测)。步长为零没有数学定义,Python 拒绝猜测;这是切片语法里唯一一种「越界之外」的硬报错。写循环遍历代码时若用了变量做步长,先别忘检查它是否为 0。

陷阱二:负步长下缺省规则反转,s[:5:-1] 是空串

s = "abcdef" 时,s[:5:-1] 返回空串——start 缺省为 len(s)-1 = 5,与 stop 重合。直觉上「从头到第 5 个字符倒着取」应该有一串字符,但 start 的缺省值在负步长下是串尾而不是 0。想要「从尾部取到下标 4」,正确写法是 s[5:4:-1](或 s[:4:-1] 得到 fedc)。遇到负步长想不清时,用 slice(None, 4, -1).indices(len(s)) 让 Python 告诉你会取哪些下标。

陷阱三:终点歧义——「想连首字符一起倒着取」

s[-1:0:-1] 返回 fedcb,缺少首字符 a:0 是开区间边界,倒着走时 0 同样不包含。要包含首字符,要么省略 stop(s[-1::-1]),要么用负步长的「走到 -1 为止」缺省(s[::-1])。这是回文检测相关 bug 的常见来源。

陷阱四:start 在 stop 之后(正步长)安静返回空串

s[4:2] 返回空串而不是报错,加上越界钳制的规则,很容易写出「运行正常但结果为空」的代码。排查套路:先打印 len(s) 核对边界实际值,或对可疑表达式调用 slice(a, b, c).indices(len(s)) 看规范化后的区间。注意这与「单索引越界抛 IndexError」是两套规则。

陷阱五:想在字符串上做「切片赋值」

long[1:3] = "xx" 抛 TypeError——str 不可变,没有切片赋值入口。要改一段只能重建拼接。反过来,列表支持切片赋值(lst[1:3] = [8, 9]),第六章会讲它与切片删除(del lst[::2])的组合用法。

陷阱六:用 is 比较两个字符串的内容

全切片快速路径让 s is s[:] 在本机实测为 True,初学者容易由此误以为字符串可以用 is 判断「内容相同」——但 is 比较的是对象身份而非内容:运行时构造的字符串与内容相同的字面量常常是不同对象(本机实测 "".join(["a", "b", "c"]) is "abc" 为 False,而 == 为 True;字符串驻留(interning)只对部分常量生效,规范不保证)。内容比较一律用 ==;is 只留给「是不是同一个对象」的场景,比如与 None 比较。

小结

切片 s[start:stop:step] 是在开区间 [start, stop) 上的等差采样:负索引等价于 len(s)+i;stop 永远不包含;越界静默钳制而单索引越界报错;步长为负时方向与缺省规则同步反转(s[::-1] 反转、s[:5:-1] 是空串);字符串切片返回新串(复制 O(k),CPython 对全切片有返回原对象的实现优化),不能切片赋值。slice 对象与 indices 方法让切片参数可复用、可调试。这些规则与列表完全一致,是第六章序列操作的地基。

练习与思考题

  1. 不运行代码,写出以下表达式的结果,再验证:"0123456789"[7::-2]、"0123456789"[:8:3]、"abcde"[-1:-6:-1]、"abcde"[:-2:-1]。
  2. 用切片一行实现:取 "https://example.com/path/page.html" 的最后两级域名 “example.com”(提示:先 rpartition 或按 ”/” 切,再用切片收尾——允许组合本篇之外的方法)。
  3. 思考题:为什么字符串切片必须产生新对象而不能像某些语言那样返回「视图」?用 str 的不可变性与哈希缓存解释(提示:第二章讲过哈希值恒成立是字典键安全的原因)。
  4. 动手题:写一个函数 every_nth(s, n) 用步长切片抽取,并验证对任意 n ≥ 1 有 len(every_nth(s, n)) 与 n 的关系式;再用 slice 对象重构它,体会参数复用的价值。