返回首页
## 引言
上一篇我们用真值测试为 if 语句铺了路,其中一个关键结论是「空字符串为假」。现在正式进入字符串(str)本身:这是第二章「变量、类型与运算符」的第四种类型,也是你职业生涯里打交道最多的数据类型——日志、配置、网络报文、用户输入,全都要先变成字符串才能处理。
本篇讲三件事:字符串字面量怎么写(四种写法)、字符串里的反斜杠到底意味着什么(转义与原始字符串)、以及索引与切片的入门(深度的切片技巧会留到第五章字符串专题)。读完本篇,你将能在不查文档的情况下写出正确的 Windows 路径写法、多行文本写法,并理解为什么 `"\u4e2d"` 打印出来是「中」而不是一串字母。同时记住一个贯穿全篇的底层事实:字符串是**不可变对象**——第一篇讲过不可变与可变的区别,这里它将变成真实的工程约束。
## 概念与原理
### str:不可变的 Unicode 码点序列
字符串在 CPython 里是 str 类型对象,本质是**元素的序列**,元素是 Unicode 码点(code point)。一个中文字符是一个码点,一个 emoji 表情也可能是一个码点(比如 😀 是 U+1F600)。所以 len() 数的是码点个数而不是「字符显示宽度」,更不是字节数——字节数与编码有关,这个话题(UTF-8 怎么把一个码点编码成一到四个字节)在第五章的系统讲解里展开,本篇只认「str 是码点序列」这一层。
三个长度概念最容易混:码点个数(len 的结果)、UTF-8 编码后的字节数、屏幕上的显示宽度。同一个汉字「中」是 1 个码点,UTF-8 编码后占 3 个字节;英文字母 a 是 1 个码点 1 个字节;组合型 emoji 可能占多个码点,显示宽度又另说——本机实测 len("中".encode("utf-8")) 为 3,len("a".encode("utf-8")) 为 1,两者与 len 的比值都不一样。跨系统核对「字数」时,必须明确口径是码点、字节还是显示宽度,否则字符校验会给出匪夷所思的结果。
因为不可变,所有看起来「修改了字符串」的操作(upper、replace、拼接 +)都是**创建新对象并重新绑定名字**,原对象纹丝不动。这与第一篇的绑定模型完全一致:s.upper() 返回新字符串,s 本身的值不变,除非你把结果重新绑定给 s。
不可变听起来像限制,实则是保护:字符串因此可以安全地用作字典键(哈希值算出来就恒成立,这正是第一篇提到哈希结构依赖「值不变」的原因)、可以大方地跨线程共享(没有就地写入口,自然没有竞态)、可以作为函数默认参数反复使用而不担心被意外污染。反观可变容器做字典键会直接抛 TypeError(unhashable type)——两相对比,不可变性的工程价值一目了然。CPython 用对象池与引用计数为「多创建几个字符串对象」兜底,代价可控,所以工程规范是:宁可多产生几个字符串,也不在设计上留出可变的共享数据。
### 字面量的四种写法
- **单引号** '...' 与**双引号** "..." 完全等价,可互相嵌套('他说"你好"' 就不用转义);
- **三引号** '''...''' 与 """...""" 允许跨行,常用于文档字符串(docstring)与长文本,行首的空白会照单全收;
- **相邻字面量自动拼接**:"a" "b" 两个相邻的字符串字面量在**编译期**就被合并成 "ab",与运行时拼接不同,它发生在程序跑起来之前,零开销。
需要强调:相邻字面量拼接只对「字面量」生效——变量和字面量混排不会触发,字符串与其他类型也不会。它的典型应用是把长 SQL 或正则拆成多行书写,但也是「列表里忘写逗号」一类 bug 的温床,第五节细说。
### 转义:反斜杠是「换挡键」
字符串字面量里的反斜杠 \ 表示「转义」(escape):它和后面的字符组合成一个特殊字符。常用清单如下:
- \n 换行、\t 制表符、\\ 反斜杠本身、\' 与 \" 引号本身;
- \xhh 十六进制字节转义(\x41 是 ASCII 的 A);
- \uhhhh 四位十六进制的 Unicode 码点(\u4e2d 是中);
- \Uhhhhhhhh 八位十六进制(\U0001F600 是 😀);
- \N{名字} 按 Unicode 名称引用(\N{SNOWMAN} 是 ☃)。
转义是在**编译期**完成的:源代码里的 "第一行\n第二行" 编译后就是一个含真实换行符的字符串对象,运行时 print 直接把它打出来。反过来说,当你写 Windows 路径 "C:\Users\new" 时,\U 后面跟的不是八位十六进制、\n 也是真实换行——这些「意外转义」要么被解释成别的含义,要么报错。这正是原始字符串登场的理由。
### 原始字符串:让反斜杠做它自己
原始字符串(raw string)写作 r"..." 或 r'...':前缀 r 告诉编译器「反斜杠不再转义」,\n 就是两个字符反斜杠加 n,原样保留。它是书写 Windows 路径、正则表达式(第五章登场)的标准工具。但原始字符串有一个铁律:**不能以单个反斜杠结尾**——因为结尾的反斜杠会把收尾的引号「转义」掉,字符串找不到终点,直接 SyntaxError。这是个语法层面的硬限制,设计上无从绕开,只能换普通字符串或拼接。
### 索引与切片:起点
字符串是序列,支持下标:msg[0] 取第一个码点,msg[-1] 取最后一个;切片 msg[0:6] 取「含头不含尾」的一段,省略起点默认为 0,省略终点默认为结尾。两个行为差异值得记住:**访问越界下标(msg[99])报 IndexError;切片越界(msg[7:99])安静地返回能取到的部分**。切片的完整语法(步长、负步长、反转)是第五章的主题,本篇先掌握「含头不含尾」这一条。
## 操作与实现
先看字面量与相邻拼接。
```python
s1 = '单引号'
s2 = "双引号"
s3 = """三引号可以
自然地跨
多行"""
same = "这段" "文本在编译期" "合并" # 相邻字面量自动拼接
print(s1, s2, sep=" | ")
print(s3)
print(same) # 这段文本在编译期合并
print(len("Python 编程")) # 9:Unicode 码点个数(6 字母 + 空格 + 2 汉字)
```
注意 len 的结果是 9,不是「字节数」,也不是「字符显示宽度」——这就是「str 是码点序列」的直接证据。
再看转义序列全家族。
```python
print("第一行\n第二行") # \n 换行
print("左\t右") # \t 制表
print('他问:"吃了没?"') # 嵌套引号,无需转义
print("他说:\"你好\"") # 转义引号
print("\\".join(["a", "b"])) # a\b:普通字符串里写反斜杠
print("\x41 \u4e2d \U0001F600") # A 中 😀
print("\N{SNOWMAN}") # ☃
```
`print("\x41 \u4e2d \U0001F600")` 的输出是 `A 中 😀`;`\N{SNOWMAN}` 输出 ☃。这些在 Python 3.10+ 上逐字可复现。
原始字符串与它的禁区。
```python
path = r"C:\Users\new\temp.py"
print(path) # C:\Users\new\temp.py:反斜杠原样
print(len(r"\n")) # 2:原始字符串里 \n 是两个字符
# r"C:\Users\nobody\" # SyntaxError:原始字符串不能以单个反斜杠收尾
# 正确姿势:普通字符串或先 r 再拼 "\\"
print("C:\\Users\\new") # C:\Users\new:普通字符串要双写反斜杠
```
r"\n" 的长度是 2,这个数字是原始字符串语义最锋利的验证:普通字符串里 "\n" 的长度是 1——同一个字面量,一个 r 前缀,长度差一倍。
索引、切片与不可变性的现场。
```python
msg = "Python 编程"
print(len(msg)) # 9
print(msg[0], msg[-1]) # P 程:负索引从尾部数
print(repr(msg[6])) # ' '
print(msg[0:6]) # Python:切片含头不含尾
print(msg[7:]) # 编程:省略终点到结尾
print(msg[7:99]) # 编程:切片越界静默截断
# print(msg[99]) # IndexError:单索引越界直接报错
```
常用方法与拼接手段。
```python
print("abc" * 3) # abcabcabc:重复
print("ab" + "cd") # abcd:拼接
print("-".join(["2026", "08", "31"])) # 2026-08-31:高频工程写法
print(" Python ".strip()) # Python:去除首尾空白
print("a,b,c".split(",")) # ['a', 'b', 'c']
print("hello".replace("l", "L")) # heLLo
s = "py"
t = s.upper()
print(s, t) # py PY:方法返回新对象,s 本身不变
```
`"-".join([...])` 是「把一组字符串用分隔符串起来」的标准答案,比循环里反复 `s = s + x` 更符合工程规范(前者一次分配,后者每次拼接都产生中间对象,虽然 CPython 在引用计数为 1 时对 += 有就地优化,但那是实现细节,不可依赖)。
最后是「漏逗号」现场。
```python
words = [
"universe",
"galaxy" "nebula", # 忘记逗号:两个字面量编译期拼成一个字符串
"star",
]
print(words) # ['universe', 'galaxynebula', 'star']
```
## 易错点与陷阱
### 陷阱一:原始字符串以单个反斜杠结尾
`r"C:\dir\"` 直接 SyntaxError,且没有任何转义技巧能在原始字符串里写出结尾反斜杠。三个正解:把结尾部分移出原始字符串(`r"C:\dir" + "\\"`)、整体改用普通字符串双写反斜杠、或接受「路径永远不以反斜杠结尾」的约定。这个错误在 Windows 上天天发生,值得刻进肌肉记忆。
### 陷阱二:普通字符串写路径,转义被悄悄吃掉
`"C:\new\test"` 里的 \n 和 \t 是换行与制表符,路径根本不成立;`"C:\U..."` 更严重,\U 后若不足八位十六进制会在编译期直接报错。**转义是编译期行为,写进字符串的那一刻就已生效**,运行时打印救不回来。规则只有一条:路径一律用原始字符串 r"...",或调用 pathlib(第七章)等路径库。
### 陷阱三:多行列表漏写逗号,触发相邻字面量拼接
`["a", "b" "c"]` 看起来三个元素,实际是 `["a", "bc"]`。相邻拼接是编译期行为,没有运行时异常——程序「正常跑」,行为却是错的,比报错难查十倍。排查手段:逐个元素打印或 alert(len(words)),养成字符串列表每行都带逗号的习惯(包括最后一行,尾逗号合法且利于 git diff)。
### 陷阱四:混淆「修改字符串」与「重新绑定」
`text = text.strip()` 之所以是惯用法,恰恰因为 strip 返回新对象、必须显式重新绑定才生效;而 `text.strip()` 单独一行自动丢弃结果,往往是逻辑 bug。这与第一篇的绑定模型一脉相承:不可变对象没有任何「原地修改」入口,一切变化都要通过「新对象 + 重新绑定」完成。老写 C 的读者尤其要警惕:这里没有 C 的「缓冲区原地写」这回事。
### 陷阱五:写了「半路转义」的普通字符串
`"\d"` 这类未知转义序列在 Python 3.12 起会在编译期给出 SyntaxWarning(本机实测:invalid escape sequence '\d'),并计划在未来版本升级为报错;3.11 及更早的版本则静默接受,反斜杠原样保留。升级 Python 后突然看到警告刷屏,多半是历史代码里藏着「本想写正则却忘了 r 前缀」的字符串——需要正则的场合请直接使用原始字符串(第五章详述),而不是靠运气让反斜杠「恰好」不被解释。
## 小结
字符串 str 是不可变的 Unicode 码点序列:字面量有四种写法,三引号支持跨行,相邻字面量在编译期自动拼接;反斜杠负责转义,\n、\u4e2d、\N{SNOWMAN} 各司其职;原始字符串让反斜杠回归字面,代价是不能以单个反斜杠收尾。索引从 0 开始、支持负数,切片含头不含尾且越界静默;所有方法都返回新对象,修改必须重新绑定。下一篇将把「怎么把别的值变成字符串」这件事讲透——f-string 与格式化三代的演进。
## 练习与思考题
1. 分别写出 r"\t"、"\t"、r"\\"、"\\" 四个字面量的长度,再运行验证。
2. 用三引号写一段含中英文、制表符与 😀 的多行文本并打印,然后解释为什么三引号里出现的 ''' 必须转义。
3. 给定路径 "C:\Users\李雷\文档\a.txt",用原始字符串构造并打印,再用 len 验证它正好是 20 个码点。
4. 思考题:`print("A" "B")` 输出 AB 而 `x = "A"; print(x "B")` 报错——请用「相邻字面量拼接发生在编译期、只对字面量生效」解释两者的差别。