Skip to content
This page has been auto-translated and may contain errors.View in English

字符串 ​

几乎你写的每个程序都会用到文本。名字、消息、分数、标签……在 Python 中,任何一段文本都叫字符串:只要用引号括起来的值都是字符串。单引号还是双引号,用法完全一样。

字符串是 Python 中最主要的文本类型,从用户名到 URL 路径,再到格式化输出,几乎无所不包。单引号和双引号产生的结果完全相同,选哪个只是风格问题。

str 出现在系统的每一个边界上:终端输入输出、文件内容、网络响应、序列化数据……它是 Python 中不可变(创建后无法更改)的 Unicode 序列类型。两种引号风格产生的是同一种对象,选哪种纯粹是风格问题。

python
greeting = "Hello, world"
username = 'alice'

引号的选择唯一要紧的时候,是你的文本里本身就含有引号。这时用另一种引号,就不用去转义了:

社区惯例是用双引号。切换风格的实际理由是当内容里正好含有那个字符时,可以避免转义:

惯例是用双引号,Black、Ruff 这类格式化工具也会自动强制这一点。唯一需要手动切换的理由,是内容里恰好含有分隔符本身,为了避免反斜杠转义:

python
note = "It's a great day"      # 里面有单引号,用双引号包裹
message = 'She said "hello"'   # 里面有双引号,用单引号包裹
escaped = "She said \"hello\""  # 或者用反斜杠转义

不可变性 ​

字符串是不可变的:一旦创建,就无法改变。可以把字符串想象成一旦生成就永久固定不变。任何看起来像是在修改字符串的操作,其实都是生成了一个全新的字符串,原来的那个丝毫未变。

字符串是不可变的:没有任何方法会就地修改字符串。每个用来转换文本的操作都会返回一个新的字符串,原字符串保持原样。这带来的实际后果是:一个方法调用如果没有赋值给任何东西,就不会产生任何效果。

str 对象是不可变的,意味着它的内容一旦创建就固定不变,没有任何操作能写入其中。这带来了两个你实际会用到的特性:它们是可哈希的(内容永远不变的值可以按内容归档,这就是字符串能作为字典键或集合成员的原因),并且在不同变量名之间传递时是安全的,因为没有人能在你不知情的情况下修改共享的字符串。

python
name = "alice"
name = name.upper()   # "ALICE" 是一个新字符串;"alice" 本身没有变

直接的后果是:你不能修改某个特定位置的字符。如果你尝试这样做,Python 会报错。

python
name = "alice"
name[0] = "A"   # TypeError: 'str' object does not support item assignment

要得到修改后的字符串,需要用切片或方法构建一个新的。这两种方式下面都会讲到。

尝试对字符赋值就能直接看出这个限制:

python
name = "alice"
name[0] = "A"   # TypeError: 'str' object does not support item assignment

当你需要一个修改后的版本时,标准做法是:位置性的修改用切片加拼接,替换用 replace()。两种方式都会产生新字符串,原字符串保持不变。

对某个位置赋值(name[0] = "A")每次都会引发 TypeError,没有例外。要做位置性的修改,可以围绕要改的部分切片来构建新字符串:name[:1].upper() + name[1:] 就能把首字母大写。要在字符串中的任意位置做替换,用 replace()。

Juno不可变性 字符串一旦存在就永远不变。任何看起来像是在编辑它的操作,实际上都是给你一个新字符串,所以一定要把结果赋值出去,不然它就消失了。刚学的时候最容易踩的坑:name[0] = "A" 这样写是不行的,会引发 TypeError。
Juno不可变性 没有任何字符串方法会就地修改,每一个都返回新字符串,原来的那个不动。所以单独写一句 name.upper() 什么用也没有,你得把结果赋值出去。而且 name[0] = "A" 会引发 TypeError,字符是没法就地替换的。
Juno不可变性 正是不可变性让字符串可哈希,从而能作为字典的键使用,也让它可以在不复制的情况下安全共享。实际影响是:每个转换方法的结果都要赋值,因为没有一个会改动原对象。要做位置性的修改,就围绕那个位置切片再重新拼接,因为下标赋值永远会引发 TypeError。

索引与切片 ​

字符串中的每个字符都有一个编号位置,从零开始。把这个位置编号放进方括号里,就能读取单个字符。负数则是从末尾往前数。

字符串是从零开始索引的序列。负索引是从末尾往回数。切片可以用一个表达式取出任意连续的一段,而且取值超出范围也不会报错。

字符串的行为就像一个可以索引和切片的有序序列。读取单个位置(s[i])时,如果索引超出了末尾,会引发 IndexError。切片(s[start:stop:step])的行为不同:超出范围的边界会被悄悄地限制到现有范围内,所以切片永远不会引发 IndexError。这个差异是要记住的实际要点:索引访问是严格的,切片是宽容的。

python
word = "Python"
#       012345

print(word[0])    # "P"
print(word[2])    # "t"
print(word[5])    # "n"
print(word[-1])   # "n"  (最后一个字符)
print(word[-2])   # "o"  (倒数第二个)

-1 永远是最后一个字符,-2 是倒数第二个,以此类推。当你想拿到字符串末尾的内容而又不想计算它的确切长度时,这很有用。

负索引是环绕计算的:-1 等于 len(s) - 1,-2 等于 len(s) - 2。当你不想手动算长度、又想从末尾开始取值时最好用。负索引超出范围时同样会引发 IndexError,和正索引一样。

负索引在边界检查之前会被转换成 len(s) + i,所以 -1 对应最后一个字符,-2 对应它前面那个。无论正负,超出范围都会引发 IndexError:只有切片才会宽容处理,普通索引不会。

切片用来取出一段。[start:stop] 包含 start,但不包含 stop:

python
word = "Python"

print(word[0:2])   # "Py"     (位置 0 和 1)
print(word[2:])    # "thon"   (从位置 2 到末尾)
print(word[:3])    # "Pyt"    (从开头到位置 2)
print(word[:])     # "Python" (整个字符串的一份拷贝)
print(word[::2])   # "Pto"    (每隔一个字符取一个)
print(word[::-1])  # "nohtyP" (反转)

最常用的三种模式:word[:n] 取前 n 个字符,word[n:] 取从位置 n 开始的所有字符,word[-n:] 取最后 n 个字符。word[::-1] 会反转字符串,第一次看到会觉得有点奇怪,但这是很地道的 Python 写法,你会经常见到它。

和直接索引不同,切片永远不会引发 IndexError。Python 会悄悄地把超出范围的索引限制到有效范围内,所以在一个短字符串上写 word[100:],得到的是 "" 而不是报错崩溃。step 参数控制步长:word[::2] 每隔一个字符取一个,word[::-1] 则反向遍历。

在 s[start:stop:step] 中,任何省略的部分,默认值都是"当前方向上自然的终点",而不是固定的 0 和 len()。step 为正时意味着从头到尾;step 为负时则反过来,start 变成最后一个字符,stop 一直延伸到开头之前。这就是为什么 s[::-1] 能不写任何边界就把整个字符串反着走一遍,同理 s[::-2] 也能让你反向每隔一个取一个字符。

Juno索引与切片 位置从零开始,所以 word[0] 是第一个字符,word[-1] 是最后一个。切片取的是一段范围:word[start:stop] 包含 start,正好在 stop 之前停下。word[::-1] 能反转字符串,第一次看会觉得挺奇怪,用过之后就会一直用下去。
Juno索引与切片 索引从零开始,或者用负数从末尾数,word[-1] 就是最后一个字符。切片取的范围包含 start、不包含 stop,第三个参数 step 设置步长,所以 word[::-1] 能反转。值得记住的一个坑:索引超出末尾会引发 IndexError,但切片超出范围只会返回现有的内容,不会报错。
Juno索引与切片 普通索引很严格,超出范围会引发 IndexError;切片很宽容,会限制在现有范围内,所以 word[100:] 返回的是 "" 而不是报错。省略某个切片部分时,它会取步长方向上自然的终点,这正是 word[::-1] 能不写任何边界就反转的原因。想避免在空输入或短输入上崩溃时,就用切片。

常用字符串方法 ​

字符串自带一组内置方法:可以直接在任意字符串值上调用的操作。写法是先写字符串(或者装着字符串的变量),再写一个点,然后是方法名。每个方法都会返回一个新字符串,原字符串永远不会被改变。

字符串方法是绑定在 str 类型上的函数。因为字符串是不可变的,每个方法都会返回一个新字符串,而不是修改原字符串。一个方法调用如果没有被赋值或传递到别处,就不会产生持续的效果。

每个进行转换的方法都会返回一个新的 str,原字符串保持不变,这直接源于不可变性。它们也是基于码点(字符本身,而不是用于存储它们的原始字节)工作的,所以带重音符号的文字和非拉丁文字,方法都能正确处理,不需要你做任何特殊操作。要留意的权衡是:每个方法都会分配一块新字符串,所以在大段文本上串联调用很多方法,就意味着很多次内存分配。

大小写 ​

python
text = "Hello, World"

text.lower()       # "hello, world"
text.upper()       # "HELLO, WORLD"
text.title()       # "Hello, World"  (每个单词首字母大写)
text.capitalize()  # "Hello, world"  (只有第一个单词)

lower() 和 upper() 是你最常用的两个。lower() 在比较文本时特别有用:"Alice" 和 "alice" 在两边都调用 .lower() 之后就变成一样的了。

lower() 是比较或存储之前的标准归一化步骤。title() 会把每个单词的首字母大写,但它用的规则比较粗糙,遇到缩略形式会出错:"it's" 会变成 "It'S"。把它当作仅用于展示的格式化即可。

lower() 应用的是 Unicode 完整大小写转换。对于大小写无关的比较,casefold() 更准确:它会应用一些 lower() 会跳过的额外转换(比如德语的 ß 会变成 ss)。title() 是在任何非字母数字字符之后都会大写一次,所以对缩略形式和带连字符的名字处理都不对。想要正确的标题大小写,得自己手动实现这个逻辑。

空白字符 ​

python
text = "  hello  "

text.strip()    # "hello"    (两端都去掉)
text.lstrip()   # "hello  "  (只去左边)
text.rstrip()   # "  hello"  (只去右边)

strip() 会去掉字符串两端的空格。处理用户输入或来自文件的文本时几乎都要用到它,因为多余的空格会造成不易察觉的错误:"alice" != "alice "。

strip() 会去掉开头和结尾所有的空白字符:空格、制表符、换行符。带方向的变体让你只清理某一侧,比如只去掉结尾的换行符而不影响缩进。这三个方法都可以传一个可选的字符参数,用来去掉指定的字符,而不是空白字符。

不带参数时,strip() 会去掉两端所有类型的空白字符,包括非 ASCII 的空白字符,不仅仅是普通空格。带字符参数时,它会去掉两端匹配这些字符中任意一个的部分,这里有个陷阱:这个参数是要去掉的字符集合,不是要匹配的前缀。"xxhelloxx".strip("x") 返回 "hello",但 "https://".strip("https") 并不会去掉前缀 "https",它会去掉两端所有的 h、t、p、s,结果是 "://"。要去掉已知的前缀或后缀,用 removeprefix() 和 removesuffix()。

查找 ​

python
text = "Hello, world"

text.find("world")         # 7
text.find("Python")        # -1  (没找到)
text.count("l")            # 3
text.startswith("Hello")   # True
text.endswith("world")     # True

find() 返回某段文本在字符串中开始出现的位置。如果不存在,就返回 -1。当你只想知道字符串是不是以某个特定内容开头或结尾时,用 startswith() 和 endswith()。

find() 返回第一个匹配项的起始索引,找不到就返回 -1。这个 -1 的约定让你可以直接把结果用在切片或算术运算里,不用先做检查。startswith() 和 endswith() 都可以接受一个字符串元组,让你在一次调用里检测多个前缀或后缀。

find() 从左到右扫描,找不到匹配就返回 -1。index() 做的是同样的查找,但会引发 ValueError 而不是返回 -1:当找不到匹配意味着代码出了 bug、你想让它立刻停下时,选 index();当缺失是正常情况、你要主动检查时,选 find()。对于纯粹"是不是以这个开头或结尾"的问题,startswith() 和 endswith() 表达的意图更明确,而且一遇到不匹配就停止,所以这种场合比用 find() 或 in 检查更合适。

替换 ​

python
text = "Hello, world"

text.replace("world", "Python")   # "Hello, Python"
text.replace("l", "L")            # "HeLLo, worLd"  (所有出现的位置)
text.replace("l", "L", 1)         # "HeLlo, world"  (只替换第一个)

replace() 会把某段文本的所有出现都替换成另一段文本,并返回一个新字符串,原字符串不会变。如果只想替换第一次出现的位置,可以传第三个参数。

replace() 默认会替换所有不重叠的匹配项。count 参数可以限制替换的次数。因为它返回的是新字符串,所以可以链式调用:text.replace("a", "A").replace("e", "E") 会依次应用两次替换。

replace() 匹配的是字面子串,不是模式,所以它不做正则匹配,把参数中的每个字符都当作字面字符处理。它从左到右替换不重叠的匹配项,这一点可能会让你意外:"aaa".replace("aa", "b") 得到的是 "ba",不是 "bb",因为第二个 aa 和第一次匹配是重叠的。count 参数限制替换次数,因为每次调用都会返回新字符串,所以可以链式调用:text.replace(",", "").replace(" ", "_")。

拆分与拼接 ​

split() 会按照一个分隔符把字符串切成几段,并以列表形式返回。你要告诉它按什么来切:

split() 会按分隔符切分,并把切出的片段以列表形式返回。不带任何参数调用时,它会按任意一段连续空白字符切分,并去掉因多个连续空格产生的空字符串:

split(sep) 从左到右扫描,在每一处不重叠出现的 sep 处切分。不带参数时,它用的是另一种算法:按任意一段连续的空白字符切分,并去掉结果两端的空白。rsplit(sep, n) 从右边开始切分,适合用来单独取出带点号路径或带命名空间标识符的最后一段。

python
csv_row = "小明,28,北京"
parts = csv_row.split(",")     # ["小明", "28", "北京"]

"  hello   world  ".split()   # ["hello", "world"]

split() 返回的是一个列表,即有序的一组值。列表会在专门的 列表 章节详细讲解;现在你只需要把它当作 split() 产生、join() 消费的那一串片段即可。

join() 做的是相反的事:把一个字符串列表合并成一个字符串。放在 .join() 前面的字符串,会被插入到每个项目之间:

python
words = ["Hello", "world"]

" ".join(words)    # "Hello world"
", ".join(words)   # "Hello, world"
"".join(words)     # "Helloworld"

要记住的模式:分隔符.join(字符串列表)。分隔符在左边,列表在右边。" ".join(words) 在每个单词之间放一个空格。"".join(words) 则把它们直接粘在一起,中间什么都不加。

只要你要把多个片段组装成一个字符串,join() 就是正确的工具。它只做一次内存分配,而不是每一步都创建一个新字符串。对于两三个字符串来说,用 + 完全没问题。但一旦你手上有一个规模不小的列表,就该用 join()。

join() 只遍历一次片段,算出总大小,一次性分配好结果所需的内存,然后把内容全部写进去。而在循环里用 + 重复拼接同样的字符串,则是代价高昂的做法:因为字符串不能被修改,每一次 + 都会分配一个新字符串,把之前所有内容都复制进去,所以成本会随片段数量的平方增长。片段数量少的时候没人会注意到,但到了几千个的时候,一个本该很快的操作就会变得很慢。避免这个问题的规则是:把片段收集到一个列表里,最后统一 join() 一次,永远不要在循环里对字符串做 +=。

Juno常用字符串方法 这些方法每一个都会返回新字符串,所以一定要把结果赋值出去,不然它就没了。你每天都会用到的几个:.lower() 和 .upper() 处理大小写,.strip() 去掉多余的空格,.find() 定位文本(找不到时返回 -1),.replace() 替换文本,还有 .split() 配合 sep.join() 拆开字符串再拼回去。
Juno常用字符串方法 这些方法全都返回新字符串,没有一个会做原地修改,所以要链式调用或者赋值它们。记牢几个坑:.strip("x") 去掉的是一组字符,不是前缀;.split() 不带参数时会把连续的空白字符压缩成一个分隔点。要组装很多片段时,先收集到一个列表里,再用 sep.join()。
Juno常用字符串方法 真实代码里最容易踩的两个坑:.strip(chars) 去掉的是两端的一个字符集合,不是前缀,想去前缀就用 removeprefix();在循环里用 += 拼字符串是慢做法,正确做法是收集到列表里再统一 join()。比较时用 .casefold() 而不是 .lower() 来归一化,.title() 只适合用来展示,因为它会把缩略形式搞乱。

f-string ​

f-string 可以把值直接嵌入到文本中。在开头的引号前加上 f,然后把任意变量或表达式用花括号括起来。Python 会在代码运行时把值填进去。你还可以在值后面加个冒号来控制它的显示方式。

f-string 会在运行时对 {} 里的任意表达式求值,并把结果转换成字符串。花括号里的冒号引入一个格式说明符:一种紧凑的语法,用来控制小数位数、对齐方式和数字格式。

f-string 中的每一个 {} 最终都会调用值自身的 __format__ 方法(一个双下方法,Python 在格式化值时在幕后调用的双下划线方法),并把冒号后面写的内容一并传进去。所以你写的任何类,都可以通过定义 __format__ 来决定它在 f-string 里的显示方式。转换标志 !r、!s 和 !a 会先对值调用 repr()、str() 或 ascii();值得记住的是 !r,因为它显示的是你在代码里输入这个值时的样子,包括引号和转义字符,下面的实例会展示这一点。

python
name = "小明"
score = 94.5

print(f"你好,{name}!")           # "你好,小明!"
print(f"分数:{score:.1f}%")      # "分数:94.5%"
print(f"2 + 2 = {2 + 2}")          # "2 + 2 = 4"
print(f"姓名:{name.upper()}")     # "姓名:小明"

冒号 : 后面的格式说明符控制值的显示方式:

说明符含义示例
.2f保留 2 位小数f"{3.14159:.2f}" → "3.14"
.0%百分比,不保留小数f"{0.94:.0%}" → "94%"
,千位分隔符f"{1000000:,}" → "1,000,000"
>10在 10 个字符宽度内右对齐f"{'hi':>10}" → " hi"

.2f 是你最常用的:任何时候你展示一个小数,想要一个整洁的数字而不是一长串数字时都会用它。表格里的其他内容会在需要时用到。你可以在 {} 里放任意变量、算术表达式或方法调用。

.2f 和 .0% 覆盖了绝大多数展示格式化的需求。对齐说明符(>、<、^)配合宽度一起用,可以生成表格式的输出。通用模式是 {value:[align][width][.precision][type]}。一旦你认清了这几部分,任何说明符都能看懂,不需要死记硬背所有组合。

同一个说明符对不同类型可能意味着不同的东西,因为每种类型都是按自己的方式来格式化的:f"{'hi':5}" 会在文本右边填充,而 f"{42:5}" 会在数字左边填充,同样是 :5,结果却相反。值得养成习惯用的转换标志是 !r:它会在格式化之前先调用 repr(),这会给字符串加上引号,并把不可见字符(制表符、结尾空格、换行符)变成可见的转义序列。当输出看起来有点微妙的不对时,把 {value} 换成 {value!r} 是最快看清里面到底是什么的方法。

Junof-string 在开头引号前加个 f,然后把任意变量、求和结果或方法调用放进 {},代码运行时 Python 就会把结果填进去。花括号里的冒号控制显示效果::.2f 保留两位小数,这个你会经常用。比用 + 把文本拼在一起清爽多了。
Junof-stringf"..." 会对 {} 里的任意表达式求值,结果直接填进去,不需要 str()。冒号后面就是格式说明符::.2f、:,、:>10 等等,全都遵循 {value:[align][width][.precision][type]} 这个结构。把这几个部分弄明白一次,以后看任何说明符都能读懂,不用死记硬背。
Junof-string 每个 {} 调用的都是值自身的 __format__,所以冒号后面的说明符具体是什么意思,由那个类型自己决定,这就是为什么 :5 对文本和数字的填充方向相反。值得留下的习惯是 !r:它显示的是你在代码里输入这个值时的样子,所以只要输出看起来有问题,多余的空白之类看不见的东西一下子就暴露出来了。

多行字符串 ​

要写一个跨多行的字符串,用三重引号:开头三个 ",结尾也三个。Python 会把你写的所有换行和空格原样保留。

三重引号字符串会原样保留所有的空白和换行。它们常用于长文本块,比如邮件模板、SQL 查询,也常用于文档字符串:放在函数或类主体开头的内联文档。

三重引号字符串会原样保留每一个字符,包括每行开头的空格,这正是要注意的地方:为了让字面量和代码对齐而加的缩进,最终会落进文本内容里。当它是函数、类或模块的第一条语句时,Python 会把它作为该对象的文档字符串(help() 显示的文档),所以惯例是在那里写一段简短的概述。如果想让三重引号块的缩进和周围代码对齐,又不想让缩进渗进值里,可以在运行时用 textwrap.dedent() 把它去掉。三重的 ''' 和 """ 效果相同;惯例是用 """。

python
message = """
亲爱的小明,

感谢您的订购。

祝好,
团队全体成员
"""
Juno多行字符串 每端各三个引号,字符串就能跨越好几行,Python 会把你打的每一个换行和空格原样保留。当你手上有一大块文本,比如一条消息或一个模板,一行放不下时,就用它。
Juno多行字符串 三重引号会原样保留换行和空格,所以适合邮件模板、SQL 查询以及各种长文本块。文档字符串也是这么实现的:放在函数或类顶部的三重引号字符串就会成为它的内联文档。要留意缩进,因为你为了对齐加的空格最终会跑进文本内容里。
Juno多行字符串 三重引号里的所有内容都会原样保留,包括开头的空白,这就是陷阱所在:为了对齐代码而加的缩进,会落进值本身里。在函数或类顶部放一段简短说明,它就会成为 help() 显示的文档字符串。当你需要它在源代码里保持缩进,但在值里又干净整洁时,用 textwrap.dedent() 处理一下。

转义序列 ​

有些字符很难直接在字符串里打出来。Python 用转义序列解决这个问题:一个反斜杠后面跟一个代表某种含义的字母。你会一直用到的两个是 \n(换行)和 \t(制表符)。

转义序列让你能嵌入那些本来会破坏语法、或者无法直接打出来的字符。你会经常用到的:\n(换行)、\t(制表符)、\\(一个真正的反斜杠)、\" 和 \'(在匹配分隔符的字符串里写引号)。Windows 路径需要用反斜杠,这会和转义处理产生冲突。加个 r 前缀就能关掉转义处理。

除了日常用到的 \n 和 \t,Python 还支持 Unicode 转义,用来表示那些没法直接打出来的字符:\uXXXX 和 \UXXXXXXXX 按编号指定字符,\N{name} 则按名字指定,比如 \N{GREEK SMALL LETTER ALPHA}。真实代码里最值得用的是原始字符串,写作 r"...",它会完全关闭转义处理,让每个反斜杠原样传递。用它来处理 Windows 路径和正则表达式模式,因为这些反斜杠本来就是给那个消费者看的,不是给 Python 看的,忘了加 r 会产生悄无声息匹配错误内容的模式。

序列字符
\n换行
\t制表符
\\字面反斜杠
\"双引号
\'单引号
python
print("Line one\nLine two")        # 输出两行
print("姓名:\t小明")              # 姓名:   小明
path = r"C:\Users\小明\Documents" # 原始字符串,不做转义处理
Juno转义序列 字符串里的反斜杠意味着"接下来这个字符要按特殊方式解读":\n 开始新的一行,\t 插入一个制表符,\\ 是一个真正的反斜杠。这两个,\n 和 \t,是你实际会打的。在引号前加个 r,反斜杠就恢复成普通字符了,处理 Windows 路径时很好用。
Juno转义序列\n、\t、\\ 和 \" 覆盖了日常会用到的转义。最能省心的是原始字符串 r"...",它把转义处理整个关掉,每个反斜杠都保持原样。用它处理 Windows 路径和正则表达式模式,因为那里的反斜杠是给别的东西用的,不是给 Python 用的。
Juno转义序列 除了 \n 和 \t,还有像 \N{name} 这样的 Unicode 转义,但真正常用的是原始字符串 r"...",它让每个反斜杠原样通过。处理正则表达式模式和 Windows 路径时用它,因为忘了加它得到的是一个悄悄匹配错误内容的模式,而不是一个明确告诉你出错的报错。

检查字符串内容 ​

Python 有一些方法能回答关于字符串内容的是/否问题,返回 True 或 False。刚开始最有用的是:isdigit() 能让你在转换之前先检查字符串是不是全是数字,避免因为意外输入而崩溃。

每个 is* 方法都会检测整个字符串的某个特定属性,只有当每个字符都满足条件时才返回 True。它们主要用于输入验证:在转换之前先检查,避免因为意外输入而崩溃。isdigit() 配合 int() 是经典模式,是 文件与异常 章节里讲到的捕获 ValueError 的另一种替代方案。

这些检查是在整个 Unicode 范围内问"是不是每个字符都属于这一类",而不只是普通的 ASCII 字母和数字。这意味着 "2".isdigit() 对上标的 2 和普通的 "2" 都返回 True,这可能让意料之外的输入溜过验证检查。当你确实只想要 0 到 9 这些数字时,组合使用 s.isascii() and s.isdigit()。isnumeric() 的范围更宽,还会把分数和其他具有数值含义的字符算进去,所以要选和你实际接受的内容最匹配的最窄的检查,而不是第一个看起来对的。

python
"42".isdigit()       # True
"hello".isalpha()    # True
"hello42".isalnum()  # True
"   ".isspace()      # True
"Hello".islower()    # False
"HELLO".isupper()    # True
Juno检查字符串内容is* 方法回答的是是非问题,只有当每个字符都符合条件时才返回 True。你最先会用到的是:在 int() 之前调用 isdigit(),确保这段文本真的是数字,这样奇怪的输入就不会让你崩溃。
Juno检查字符串内容 每个 is* 方法只有整个字符串都通过检验才返回 True,这让它们很适合在转换之前验证输入。isdigit() 配合 int() 是经典的防护写法。记住它检验的是整个字符串,所以空字符串在所有这些方法上都返回 False。
Juno检查字符串内容 这些检查覆盖整个 Unicode 范围,不只是普通 ASCII,所以 isdigit() 会让上标数字之类你没打算接受的数字字符也通过。当你确实只想要 0 到 9 时,组合使用:s.isascii() and s.isdigit()。选和你真实输入最匹配的最窄检查,而不是第一个看起来差不多对的。

实践演练 ​

先去空格,再统一大小写,然后取出你需要的内容。这套流程几乎能处理任何用户输入的文本:

python
raw_input = "  [email protected]  "
email = raw_input.strip().lower()   # "[email protected]"

at_pos = email.find("@")
username = email[:at_pos]
domain = email[at_pos + 1:]

print(f"用户名:{username}")    # "alice"
print(f"域名:{domain}")      # "example.com"

从几个部分拼出一个 URL,然后立刻验证并解析它:

python
BASE_URL = "https://api.example.com"
version = "v2"
resource = "users"
user_id = 42

url = f"{BASE_URL}/{version}/{resource}/{user_id}"
# "https://api.example.com/v2/users/42"

parts = url.split("://")              # ["https", "api.example.com/v2/users/42"]
protocol = parts[0]                   # "https"
secured = url.startswith("https")
domain = parts[1].split("/")[0]       # "api.example.com"

print(f"协议     : {protocol}")
print(f"是否加密 : {secured}")
print(f"域名     : {domain}")

用 find()、切片和 f-string 对齐来解析一条结构化的日志:

python
log_entry = "[2024-01-15 09:42:11] ERROR: File not found: report.csv"

timestamp = log_entry[1:20]
rest = log_entry[22:]                # "ERROR: File not found: report.csv"
colon_pos = rest.find(":")
level = rest[:colon_pos]             # "ERROR"
message = rest[colon_pos + 2:]       # "File not found: report.csv"

print(f"[{timestamp}] {level:>8}: {message}")
# [2024-01-15 09:42:11]    ERROR: File not found: report.csv

find() 定位分界点,切片取出各个部分,>8 这个格式说明符把严重级别标签右对齐,这样即便不同级别名字长度不同,列也能保持整齐。

方法一览表 ​

方法作用
.lower() / .upper()转换为全小写 / 全大写
.title() / .capitalize()每个单词首字母大写 / 只有第一个单词首字母大写
.strip() / .lstrip() / .rstrip()去掉两端的空白字符
.find(sub)第一个匹配项的索引,或 -1
.count(sub)sub 出现的次数
.startswith(s) / .endswith(s)前缀 / 后缀检查
.replace(old, new)替换出现的内容
.split(sep)拆分成一个列表
sep.join(iterable)把项目拼接成一个字符串
.isdigit() / .isalpha() / .isalnum()字符类型检查