前言
字符串(string)是日常写 Python 打交道最多的类型。标题里列出了七类操作——替换、删除、截取、复制、连接、比较、查找——它们覆盖了str方法的大半江山,也恰好集中了最容易被记错的几个点。
先纠正一个前提性误解:str是不可变(immutable)的。所谓「替换」「删除」,其实没有一个方法能改动原字符串,所有返回字符串的方法都是返回一个新字符串,原对象纹丝不动。理解这一点,后面所有的行为都能推导出来:既然不能改,那么「原地修改」不存在,「复制」这件事对字符串也就没有深浅之分——赋值和切片得到的都指向同一个不可变对象,安全得很。
第二个误解是「字符串切片越界会报错」。实际规则是分成两半:切片越界不报错,索引越界才报错。"abc"[10:20]老老实实返回空字符串,而"abc"[10]立刻IndexError。这条差别是很多 bug 的源头。
本文按标题的顺序,把七类操作逐个讲清,并标注容易记错的细节。
一、查找:find / index / count / in
查找分「找位置」和「判断存在」两种需求,官方文档明确建议:只想知道子串在不在,用in;需要位置时才用find。
| 写法 | 找不到时 | 返回 |
|---|
s.find(sub) | 返回-1 | 首次出现的下标 |
s.index(sub) | 抛ValueError | 首次出现的下标 |
s.rfind(sub)/s.rindex(sub) | 同上 | 最后一次出现的下标 |
sub in s | 返回False | True/False |
s.count(sub) | 返回0 | 出现次数 |
三种方法都接受可选的start、end,语义和切片一致:
# 适用于 Python 3.8+
s = "spam, spam, spam"
print(s.find("sp")) # 0
print(s.find("sp", 5)) # 6
print(s.find("egg")) # -1
print(s.count("spam")) # 3
print(s.startswith("spam")) # True
print(s.endswith(("am", "gg"))) # True —— 可以传元组startswith()/endswith()的第一个参数可以是一个元组,只要匹配其中任意一个就返回True,写多个分支判断时很方便。
二、截取:切片是主力
「截取」在 Python 里就是切片(slicing),语法s[start:stop:step],三个部分都可省略:
# 适用于 Python 3.8+
s = "Python"
print(s[1:4]) # 'yth'
print(s[:3]) # 'Pyt'
print(s[3:]) # 'hon'
print(s[-2:]) # 'on' 负索引从末尾数
print(s[::-1]) # 'nohtyP' 步长为负即反转
print(s[::2]) # 'Pto' 隔一个取一个越界行为是这里的关键:切片的下标超出范围会被自动裁剪,不报错。
# 适用于 Python 3.8+
s = "abc"
print(s[10:20]) # '' —— 越界切片得到空串
print(s[100]) # IndexError: string index out of range
print(s[-100:]) # 'abc' —— 负向越界也会被裁剪因此「安全取子串」用切片,而「访问单个字符」必须先判断长度。s[0]得到的是长度为 1 的字符串,Python 里没有单独的字符类型。
三、替换:replace 与 translate
str.replace(old, new, count=-1)返回新字符串,把old全部替换成new;给了count就只替换前count次。注意count在 Python 3.13 之前只能是位置参数,3.13 起才支持关键字形式。
# 适用于 Python 3.8+
s = "spam, spam, spam"
print(s.replace("spam", "eggs")) # 'eggs, eggs, eggs'
print(s.replace("spam", "eggs", 1)) # 'eggs, spam, spam'
print(s) # 'spam, spam, spam' —— 原串没变要做逐字符替换(比如大小写转换、删掉某几个字符),用translate()配maketrans():
# 适用于 Python 3.8+
table = str.maketrans("abc", "123", " ") # a->1, b->2, c->3, 空格删除
print("a b c".translate(table)) # '123'str.maketrans()有三参数的静态方法形式maketrans(from, to, remove=''),也有单参数的字典形式maketrans({...})。replace是「子串替换」,translate是「字符映射」,两者别混用。
四、删除:strip、removeprefix、removesuffix
strip()家族删的是两端属于某个字符集合的字符,不是删「前缀」:
# 适用于 Python 3.8+
print(" spacious ".strip()) # 'spacious'
print("www.example.com".strip("cmowz.")) # 'example'strip("cmowz.")的参数是字符集合,不是前缀串,所以它会从两端一直删,直到遇到不在集合里的字符为止。这点是最大的误会来源。
真正按「前缀/后缀」删的方法在Python 3.9 及以上才有:
# 适用于 Python 3.9+
print("TestHook".removeprefix("Test")) # 'Hook'
print("BaseTestCase".removeprefix("Test")) # 'BaseTestCase' —— 不匹配就原样返回
print("MiscTests".removesuffix("Tests")) # 'Misc'3.9 之前要自己用切片实现:
# 适用于 Python 3.8 及更早
s = "TestHook"
prefix = "Test"
if s.startswith(prefix):
s = s[len(prefix):]三个方法的对比:
| 方法 | 删除依据 | 是否按完整前缀 |
|---|
strip(chars) | 两端的字符集合 | 否,逐字符判断 |
lstrip/rstrip | 左端 / 右端字符集合 | 否 |
removeprefix(p) | 完整前缀,且只删一次(3.9+) | 是 |
removesuffix(s) | 完整后缀,且只删一次(3.9+) | 是 |
五、复制与连接
复制:字符串不可变,s2 = s已经足够安全,任何一方都不会影响另一方。写s[:]或s + ""去「深拷贝」纯属多余。唯一要注意的是,字符串没有.copy()方法——那是我给读者挖的假想方法,别去找。
连接:+拼两个字符串,*重复,多个字面量相邻会自动合并:
# 适用于 Python 3.8+
print("Py" + "thon") # 'Python'
print("ab" * 3) # 'ababab'
print("abc" "def") # 'abcdef' —— 字面量隐式拼接,仅限字面量
sep = ", "
print(sep.join(["a", "b", "c"])) # 'a, b, c'拼接效率的常识:反复+=拼接大量片段,理论上是 O(n²),因为每次都要新建字符串并复制已有内容。推荐做法是把片段收集进列表,最后"".join(parts)一次性拼出结果,join在结构上是 O(n)。需要说明的是,CPython 对「对同一个变量反复+=」有一层就地扩容的优化,实际表现常常接近线性,但这属于实现细节、不是语言保证,跨解释器(如 PyPy)就可能退化,所以仍应以join为准。本文不给具体倍数,读者可用timeit自行对比。
# 适用于 Python 3.8+
parts = [str(i) for i in range(1000)]
text = "".join(parts) # 推荐六、比较
==比内容,is比身份(是不是同一个对象)。字符串驻留(interning)是实现细节,不要依赖is判断字符串相等:
# 适用于 Python 3.8+
a = "hello"
b = "hel" + "lo"
print(a == b) # True —— 内容相同
print(a is b) # 不确定,别依赖排序比较用的是码点顺序,所以大写字母排在小写字母前面,"Z" < "a"为真。做大小写无关比较时,用casefold()而不是lower():casefold是为「无差别比较」设计的,对德语 ß、土耳其语等处理更彻底。
# 适用于 Python 3.8+
print("Straße".lower().casefold()) # 'strasse'
print("ABC".lower() == "abc") # True比较还可用于排序:sorted(words)按码点升序,sorted(words, key=str.casefold)忽略大小写。
常见坑点
- 把 strip 当成删前缀
❌"www.example.com".strip("www.")以为得到example.com,实际得到example(把 e、x、a、m、p、l 也删了)
✅ 删完整前缀用 3.9+ 的removeprefix("www."),旧版用s[len(prefix):]配startswith判断
- 混淆 find 与 index 的失败行为
❌ 用s.find(x) == -1判断「没找到」写成>,或用s.index(x)后忘了它可能抛异常
✅ 判断存在用x in s;find失败返回-1,index失败抛ValueError,按需选
- 以为切片越界会报错
❌"abc"[5]用来「安全取字符」,结果IndexError
✅ 单字符用索引并先判断长度,取子串用切片(切片越界返回空串)
- 以为 replace 会改原串
❌s.replace("a", "b")之后直接用s,发现没变
✅ 接住返回值:s = s.replace("a", "b")
- 用 is 比较字符串
❌if name is "admin":在不同运行环境结果可能不同
✅if name == "admin":
- join 顺序写反
❌["a", "b"].join("-")会报AttributeError,因为列表没有 join
✅"-".join(["a", "b"]),分隔符是调用者
- 忘了 join 要求元素是字符串
❌"".join([1, 2])→TypeError
✅"".join(str(x) for x in [1, 2])
- 用 lower() 做大小写无关比较
❌if a.lower() == b.lower():对某些特殊字符不够彻底
✅if a.casefold() == b.casefold():
总结
| 操作 | 首选写法 | 记忆点 |
|---|
| 查找 | sub in s/s.find(sub) | find失败返回 -1,index抛异常 |
| 截取 | s[a:b] | 越界不报错,负索引从末尾数 |
| 替换 | s.replace(old, new, count) | 返回新串,原串不变 |
| 删除 | strip(chars)/ 3.9+ 的removeprefix | 前者是字符集合,后者才是前缀 |
| 复制 | s2 = s | 不可变,无需拷贝 |
| 连接 | sep.join(parts) | 优于大量+= |
| 比较 | ==/casefold() | is是实现细节,不要用 |
把这七类操作串起来看,会发现一条主线:字符串不可变。所有方法都返回新值,原对象永远不变;由此推导出的切片安全、赋值为共享、拼接要换join,都是同一个事实的不同侧面。记住这条主线,再记准strip的字符集合语义和find/index的失败差别,字符串操作就基本不会写错了。