Python 字符串为什么是不可变对象?常见字符串方法要注意什么?
简化版
Python 的 str 是不可变对象,创建后内容不能原地修改,任何看似修改字符串的操作都会产生新字符串。常见字符串方法如 replace、strip、lower 也不会改变原字符串,而是返回处理后的新字符串。
详细版
字符串不可变意味着:
s = "hello"
# s[0] = "H" # TypeError
t = s.replace("h", "H")
print(s) # hello
print(t) # Hello
常见方法的特点:
strip():去掉两端空白或指定字符,返回新字符串。replace(old, new):替换内容,返回新字符串。split(sep):按分隔符拆分,返回列表。join(iterable):把多个字符串连接成一个字符串。lower()/upper():大小写转换,返回新字符串。startswith()/endswith():判断前缀和后缀。find():找不到返回-1。index():找不到抛ValueError。
性能上,大量循环拼接字符串时不要反复 s += part,更推荐:
parts = ["a", "b", "c"]
result = "".join(parts)
因为字符串不可变,频繁拼接可能创建很多临时对象。
完整版教学
一、不可变到底是什么意思
不可变不是变量不能重新赋值,而是对象本身不能原地改。
s = "abc"
s = "xyz"
这段代码合法,但不是把 "abc" 这个对象改成 "xyz",而是让变量 s 重新绑定到另一个字符串对象。
真正的原地修改是不允许的:
s = "abc"
# s[0] = "A" # TypeError: 'str' object does not support item assignment
所以字符串方法大多不会修改原对象,而是返回新对象。
可以画成这样:
s ───> "abc"
s = "xyz"
s ───> "xyz" 原来的 "abc" 没被改,只是 s 换了绑定
这和 list 很不一样。nums[0] = 9 能改列表对象内部槽位;s[0] = "A" 不允许,因为字符串没有暴露原地修改字符的能力。面试回答要把“变量重新绑定”和“对象原地修改”分开讲。
二、为什么字符串设计成不可变
字符串不可变有几个好处:
- 可以安全作为字典 key 和集合元素,因为哈希值稳定。
- 多个地方共享同一个字符串对象时,不会被某处偷偷改掉。
- 解释器可以做缓存、复用等优化。
- 字符串语义更接近“值”,使用起来更安全。
例如:
cache = {"python": 1}
如果字符串可以原地修改,"python" 被改成别的内容后,字典内部哈希结构就会被破坏。
不可变还让字符串在多处共享时更安全。比如函数接收到一个字符串,不必担心另一个引用会把这个字符串的第 0 个字符偷偷改掉。解释器也可以对某些字符串做驻留或缓存优化;这些是实现优化,不应作为业务判断身份的依据,但不可变性是优化成立的重要前提。
三、replace、strip、lower 都返回新字符串
看一个典型坑:
s = " Python "
s.strip()
print(s) # " Python "
strip() 的结果没有被接住,原字符串不会变。正确写法:
s = s.strip()
print(s) # "Python"
同理:
name = "Tom"
name.lower()
print(name) # Tom
name = name.lower()
print(name) # tom
这些方法的共同点是“不改原字符串,返回处理后的新字符串”。strip(chars) 还有一个常见误解:参数不是要去掉的完整前后缀,而是“字符集合”。如果要去固定前缀,Python 3.9+ 有 removeprefix;固定后缀有 removesuffix。
print("www.example.com".strip("w.com")) # example
print("www.example.com".removeprefix("www.")) # example.com
易错点:
strip("abc")去的是两端任意a/b/c字符,不是去掉字符串"abc"这个整体。
四、split 和 join 是一对常见组合
split 把字符串拆成列表:
line = "java,python,go"
items = line.split(",")
print(items) # ['java', 'python', 'go']
join 把字符串序列连接起来:
items = ["java", "python", "go"]
line = ",".join(items)
print(line) # java,python,go
注意 join 的调用者是分隔符,不是列表:
",".join(items)
这个写法刚开始有点反直觉,但它能清楚表达“用逗号连接这些字符串”。
还要注意,join 要求参与连接的元素都是 str,不会自动把数字转成字符串。需要连接数字列表时,要先转换:
nums = [1, 2, 3]
print(",".join(str(x) for x in nums)) # 1,2,3
split() 不带参数时,会按任意空白分割,并自动合并连续空白;split(" ") 则只按单个空格分割,连续空格会产生空字符串。这是日志解析和命令行参数处理里常见细节。
五、查找、判断和大小写方法的边界
find 和 index 都能查子串,但找不到时行为不同:
s = "hello"
print(s.find("x")) # -1
# s.index("x") # ValueError
startswith、endswith 适合判断前后缀,比手写切片更直观。lower、upper 做简单大小写转换,涉及大小写无关比较时,可以了解 casefold(),它比 lower() 更适合某些 Unicode 文本的大小写折叠。不过中文面试里别把它扩太远,知道普通英文场景 lower() 常用即可。
| 方法 | 找不到/结果特点 | 常见用途 |
|---|---|---|
find | 找不到返回 -1 | 容忍不存在 |
index | 找不到抛 ValueError | 不存在是异常 |
startswith | 返回 bool | 前缀判断 |
replace | 返回新字符串 | 内容替换 |
strip | 返回新字符串 | 去两端字符 |
六、字符串拼接的性能要怎么理解
少量拼接直接用 + 或 f-string 没问题:
message = f"hello {name}"
但大量循环拼接要小心:
s = ""
for part in parts:
s += part
因为字符串不可变,每次拼接都可能创建新字符串。更常见的写法是:
result = "".join(parts)
它先收集片段,再一次性连接,语义清楚,也更适合大量字符串构造。
数字例子:循环拼接 10000 个片段时,如果每次都创建一个更长的新字符串,可能产生大量临时对象;join 可以先知道所有片段,再统一分配结果。CPython 对某些局部字符串拼接有优化,但不要把优化当成写法依赖;写生成大量文本时,list append + join 仍是清晰稳妥的模式。
七、常见误区与追问
- 误区:
s.strip()会修改原字符串。 字符串不可变,方法返回新字符串,必须接住返回值。 - 误区:
strip("abc")删除完整子串。 它删除两端任意属于字符集合的字符,固定前后缀用removeprefix、removesuffix。 - 误区:大量循环
+=拼接一定没问题。 少量可以,大量构造更推荐收集片段后join。 - 追问:字符串为什么能做 dict key? 字符串不可变,哈希值稳定,适合放进 dict/set。
- 追问:
find和index区别是什么? 找不到时find返回-1,index抛ValueError。 - 追问:
split()和split(" ")有什么不同? 前者按任意空白并合并连续空白,后者按明确空格分隔,可能产生空字符串。
八、加强记忆
把字符串记成“不可变文本值”:不能按下标原地改,replace、strip、lower 等方法都返回新字符串;查找要分清 find 和 index,拆分连接常用 split/join,大量拼接优先 join。回答时把不可变带来的哈希稳定、共享安全和方法返回新对象串起来,内容就不会散。