本节摘要:str 是不可变序列,也是 Python 里出现频率最高的类型。本节讲它的不可变设计动机(哈希、共享、驻留)、切片与常用方法、f-string 为何成为格式化首选,以及 str 与 bytes 的分工——一句话真相:str 是文本,bytes 是字节,编码是两者之间的桥。
第 2.1 节说过 tuple 不可变的红利,str 把同样的逻辑推到极致。设想如果 str 可变:把 "hello" 交给一个函数,函数改了它的第一个字符,全世界所有引用 "hello" 的地方同时遭殃——因为驻留机制下它们共享同一个对象。不可变让字符串可以放心地作为 dict 键、放心地全局共享、放心地并发读取。这也是所有主流语言(Java、C#、Go)不约而同把字符串做成不可变的根本原因。
实证一下驻留与共享:
>>> a = "hello" >>> b = "hello" >>> a is b # 标识符样式的字面量被驻留共享 True >>> c = "hello world!" >>> d = "hello world!" >>> c is d # 含空格感叹号,REPL 中各自建对象(CPython 版本行为有差异) False >>> c == d # 值比较永远可靠 True
再次强调 1.2 节的结论:is 只用来判 None,字符串比较一律 ==。
str 完整实现了序列协议,第 2.1 节的下标与切片规则原样适用:
>>> s = "Pythonista" >>> s[0], s[-1] ('P', 'a') >>> s[2:5] 'tho' >>> s[::-1] # 反转的惯用法 'atsinohyP' >>> len(s), "thon" in s (10, True)
但注意:所有"修改"方法都返回新对象,原地无感:
>>> s.upper() 'PYTHONISTA' >>> s # 原串纹丝不动 'Pythonista'
高频方法按用途归类记忆,比背列表有效:
find(找不到返回 -1)、index(找不到抛错)、startswith、endswith、in;split / rsplit / splitlines / join;strip / lstrip / rstrip、replace、zfill;isdigit、isalpha、isspace(解析脏数据时的主力)。其中 join 值得单独点名——拼接大量字符串时它是唯一的正确姿势:
>>> parts = ["id", "name", "email"] >>> ",".join(parts) 'id,name,email'
join 一次算总长、一次分配;而循环里 s += part 每步都可能造新串(现代 CPython 有原地优化,但那是实现细节,跨实现不可依赖)。两者的差距在万级拼接时可达百倍。
Python 的格式化历史足以当一面镜子:% 格式(C 时代遗产)→ str.format(2.6)→ f-string(3.6)。前两种仍能见到,但新代码没有理由不用 f-string:
>>> name, score, ratio = "Ann", 92.4567, 0.873 >>> f"{name} 得分 {score:.1f},通过率 {ratio:.1%}" 'Ann 得分 92.5,通过率 87.3%' >>> f"{name=}" # 调试神器:连名字带值一起输出 "name='Ann'" >>> f"[{score:>10.2f}]" # 右对齐宽度 10 '[ 92.46]' >>> f"[{name:*^12}]" # 居中填充 '****Ann*****'
f-string 的本质是编译期把表达式直接编译进字节码,运行时求值一次完成,比 format 少一层方法调用与解析。用 dis 可以看到 FORMAT_VALUE 指令——它不是字符串替换魔法,是语言级语法。这也解释了为什么 f-string 是三者中最快的。
格式说明符的完整语法是一条迷你语言([[fill]align][sign][width][,][.precision][type]),日常记住三件套够用:.2f 定点、.1% 百分比、>10 / <10 / ^10 对齐。复杂数字格式(千分位、进制)查表即可:
>>> f"{1234567:,}" # 千分位 '1,234,567' >>> f"{255:#x} {255:b}" # 十六进制与二进制 '0xff 11111111'
(提醒一句:f-string 里可以写任意表达式,但别塞复杂逻辑——先算好再格式化,可读性天差地别。)
Python 3 把文本与字节彻底分开:str 是 Unicode 文本的序列,bytes 是原始字节的序列;两者不能隐式互转,只能显式编码解码:
>>> text = "中文" >>> blob = text.encode("utf-8") >>> blob b'\xe4\xb8\xad\xe6\x96\x87' >>> blob.decode("utf-8") '中文' >>> text + blob TypeError: can only concatenate str (not "bytes") to str
错误信息本身就在教育你:这是两个世界。乱码的统一解释是:编码与解码用了不同的码表。Windows 下读文件常见 UnicodeDecodeError: 'gbk' codec can't decode ...——因为默认编码随平台走,而文件是 UTF-8。解法是永远显式声明:
with open("data.txt", encoding="utf-8") as f: # 第 6 章详讲 content = f.read()
判断该用哪个:来自网络、文件、加密接口的"生数据"用 bytes;要显示、搜索、切片的"文本"用 str。len 的语义也随之分裂——len("中") 是 1(字符),len("中".encode()) 是 3(UTF-8 字节)。
处理用户提供的格式模板时要小心:str.format 能访问对象属性,恶意的格式串可能泄露数据(如 {0.__class__} 之类探索链)。规则:格式串必须是开发者可控的常量;需要用户模板时用 string.Template(只支持 $name 占位,无法穿透对象)。
⚠️ 常见坑汇总:
"1" + 1直接 TypeError,混用前先转换;"abc" < "abd"是按码点比较,日期字符串必须补零(09而不是9)才能正确排序;split()无参调用按任意空白切分并去空串,与split(" ")语义不同。
💡 关键直觉:把 str 想成"刻好的石碑"——想改内容只能重刻一块(新对象)。这个模型让你对每次"修改"操作的成本心里有数。
+= 是反模式,万级拼接差百倍。name= 调试语法;格式串必须开发者可控。第 3 章进入控制流:for 循环其实不是数数,而是在向容器索要迭代器——五大容器在"迭代协议"上会师。