Effective Python 条款4:字符串格式化大乱斗
- [Bilibili 同步视频](#Bilibili 同步视频)
- [🧨 老古董:C 风格 % 格式化操作符](#🧨 老古董:C 风格 % 格式化操作符)
-
- [坑 1:顺序、类型不匹配,直接运行时报错](#坑 1:顺序、类型不匹配,直接运行时报错)
- [坑 2:填充前要预处理变量,代码疯狂膨胀](#坑 2:填充前要预处理变量,代码疯狂膨胀)
- [坑 3:同一个值多处使用,必须重复传参](#坑 3:同一个值多处使用,必须重复传参)
- [坑 4:字典模式,键名反复抄写,代码冗余爆炸](#坑 4:字典模式,键名反复抄写,代码冗余爆炸)
- [🛠️ 改良方案 str.format (),理想很丰满现实骨感](#🛠️ 改良方案 str.format (),理想很丰满现实骨感)
-
- [⚠️但是!format 没有根治老问题](#⚠️但是!format 没有根治老问题)
- [🏆王者登场 f‑string,Python3.6 + 的终极答案🎉](#🏆王者登场 f‑string,Python3.6 + 的终极答案🎉)
-
- [✨ 优势 1:大括号内直接运行表达式,预处理一步到位](#✨ 优势 1:大括号内直接运行表达式,预处理一步到位)
- [✨优势 2:格式参数也支持变量,告别硬编码](#✨优势 2:格式参数也支持变量,告别硬编码)
- ⚡补充:简单聊一嘴性能
- 📝总结与最佳实践
摘要:字符串格式化是 Python 开发中绕不开的基本功,打印日志、拼接输出、组装报文处处都要用到它。Python 一共提供了 4 套内置格式化方案,但并不是每一种都值得我们在项目中使用。今天我们就扒一扒老式
%、str.format()的各种坑,搞懂为什么f‑string 才是现代 Python 的最优解。
不知道你有没有踩过字符串格式化的坑😵💫:明明代码看着没问题,一运行直接抛出TypeError;改一个变量,模板里好几处都要同步修改;格式化代码越写越长,换行拆得七零八落,读起来像一团乱麻。
很多 Pythoner 写代码,上来就直接%百分号格式化,毕竟它源自 C 语言printf,上手简单。但好用不代表没有隐患,《Effective Python》条款 4就专门剖析了 Python 的几种字符串格式化方案,我们今天结合实战案例,聊聊各个方案的优缺点。
Bilibili 同步视频
🧨 老古董:C 风格 % 格式化操作符
%是 Python 最早的格式化手段,模板字符串左边写格式,右边用元组传入待填充的值。
示例代码:
python
a = 0b10111011 # 二进制
b = 0xc5f # 十六进制
print("Binary is %d, hex is %d" % (a, b))
# 输出:Binary is 187, hex is 3167
我们可以使用%d、%s、%x、%f这类格式符,控制数字、字符串、浮点数的输出,还能控制对齐、小数点保留位数。很多从 C/C++ 转过来的开发者会非常习惯这套语法。
但是!这套语法暗藏 4 个大坑,写项目的时候稍不留神就翻车。
坑 1:顺序、类型不匹配,直接运行时报错
元组传参,顺序必须和模板内格式符一一对应,一旦顺序颠倒,类型对不上,直接抛异常。
python
key = "my_Var"
value = 1.234
# 正确写法
formatted = "%-10s = %.2f" % (key, value)
print(formatted) # my_Var = 1.23
# ❌ 灾难:元组内两个变量写反
# reordered = "%-10s = %.2f" % (value, key)
# TypeError: must be real number, not str
💡痛点:编译器不会帮你检查,只有程序跑到这一行才炸锅。模板改了格式符顺序,右边元组也要同步修改,人工维护很容易漏改。
为了解决顺序问题,%支持传入字典代替元组,用%(键名)s绑定字典 key,不再依赖参数顺序:
python
key = "my_Var"
value = 1.234
fmt = "%(key)-10s = %(value).2f" % {"key": key, "value": value}
print(fmt)
这样确实解决顺序错乱的问题,但是会引出新的麻烦,我们往下看。
坑 2:填充前要预处理变量,代码疯狂膨胀
业务场景中,我们经常要对变量做预处理:比如数字四舍五入、字符串首字母大写。
拿食材列表打印举例:
python
pantry = [
("avocados", 1.25),
("bananas", 2.5),
("cherries", 15),
]
for i, (item, count) in enumerate(pantry):
print("#%d: %-10s = %d" % (
i + 1, # 编号+1
item.title(), # 首字母大写
round(count) # 数值四舍五入
))
输出:
Plain
#1: Avocados = 1
#2: Bananas = 2
#3: Cherries = 15
仅仅 3 个简单处理,%右侧元组就必须拆成多行。如果业务逻辑更复杂,预处理更多,整段格式化代码会变得臃肿难读。
坑 3:同一个值多处使用,必须重复传参
模板里同一个变量要出现多次,元组里面就得重复写几遍变量:
python
template = "%s loves food. See %s cook."
name = "Max"
formatted = template % (name, name)
print(formatted)
# Max loves food. See Max cook.
如果要对name做转换,比如name.title(),两个位置都要修改,漏改一处输出结果就不一致,维护成本翻倍。
✨小提示:改用字典模式可以规避重复传参,但会加剧代码冗长的问题。
坑 4:字典模式,键名反复抄写,代码冗余爆炸
当使用字典做%格式化,同一个键名,要在格式模板写一遍、字典 key 再写一遍,如果外部还有同名变量,相当于同一个名字抄写 3 次。
python
soup = "lentil"
formatted = "Today's soup is %(soup)s." % {"soup": soup}
print(formatted)
这里soup这个名字写了三次!业务模板一旦变长,体验会更难受:
python
menu = {
"soup": "lentil",
"oyster": "kumamoto",
"special": "schnitzel",
}
template = ("Today's soup is %(soup)s, "
"buy one get two %(oyster)s oysters, "
"and our special entree is %(special)s.")
output = template % menu
print(output)
模板字符串和字典分处两处,修改键名,两边必须同步改动。来回翻看模板和字典,找对应关系,bug 就很容易潜伏在这里。
📌小结:
%操作符,简单脚本临时用尚可,但正式项目尽量少用。元组模式怕顺序,字典模式怕冗余。
🛠️ 改良方案 str.format (),理想很丰满现实骨感
Python3 推出format(),意图解决%的历史遗留问题,抛弃 C 风格格式符,使用{}占位。
内置format()函数可以单独处理单个变量,支持千位分隔符、居中对齐等丰富的格式迷你语言:
python
a = 1234.5678
print(format(a, ",.2f")) # 千位分隔,保留两位小数 →1,234.57
b = "my string"
print(f"*{format(b, '^20s')}*")
# * my string *
字符串的.format()方法,可以在大括号内写索引,解除参数顺序绑定:
python
key = "my_Var"
value = 1.234
# {1}取第二个参数,{0}取第一个参数,不关心模板内顺序
out = "{1} = {0}".format(key, value)
print(out) #1.234 = my_Var
同一个索引可以多次复用,解决变量重复传入的痛点:
python
name = "Max"
out = "{0} loves food. See {0} cook.".format(name)
print(out)
大括号内支持冒号书写格式规则,还支持!r获取 repr 字符串,也支持访问字典 key、列表下标:
python
menu = {"oyster": "kumamoto"}
out = "First letter is {menu[oyster][0]!r}".format(menu=menu)
print(out) # First letter is 'k'
⚠️但是!format 没有根治老问题
虽然它修复了顺序错乱、重复传参的问题,但预处理变量时代码臃肿、键名重复抄写两大痛点依旧存在。
对比下面两段等价代码,format写法并没有比老式%清爽多少:
python
for i, (item, count) in enumerate(pantry):
old_style = '#%d: %-10s = %d' % (
i + 1,
item.title(),
round(count))
new_style = '#{}: {:<10s} = {}'.format(
i + 1,
item.title(),
round(count))
assert old_style == new_style
当我们传入关键字参数的时候,依然要重复书写变量名:soup=soup。
python
new_template = (
"Today's soup is {soup}, "
"buy one get two {oyster} oysters, "
"and our special entree is {special}.")
new_formatted = new_template.format(
soup="lentil",
oyster="kumamoto",
special="schnitzel",
)
💡作者观点:
format()只建议学习它的格式迷你语言规则(冒号后面那套对齐、精度语法),实际业务开发不推荐优先使用。
🏆王者登场 f‑string,Python3.6 + 的终极答案🎉
Python3.6 引入插值格式化字符串 f‑string ,在字符串前面加前缀f。它直接解决了上面提到全部痛点!
核心优势:大括号 **{}**内部直接写 Python 表达式,可以直接访问当前作用域所有变量,不需要额外传参、不需要构造元组、不需要写字典。
基础示例:
python
key = "my_Var"
value = 1.234
formatted = f"{key} = {value}"
print(formatted) # my_Var = 1.234
原来那套强大的格式迷你语言完全兼容,搭配!r也不在话下:
python
formatted = f"{key!r:<10} = {value:.2f}"
print(formatted) # 'my_Var' = 1.23
我们把四种写法放在一起横向对比,高下立判:
python
key = "my_Var"
value = 1.234
f_string = f'{key:<10} = {value:.2f}'
c_tuple = '%-10s = %.2f' % (key, value)
str_args = '{:<10} = {:.2f}'.format(key, value)
str_kw = '{key:<10} = {value:.2f}'.format(key=key, value=value)
c_dict = '%(key)-10s = %(value).2f' % {'key': key, 'value': value}
assert c_tuple == c_dict == f_string
assert str_args == str_kw == f_string
✨ 优势 1:大括号内直接运行表达式,预处理一步到位
还记得之前食材列表要做i+1、item.title()、round(count)吗?
在 f‑string 中,直接把表达式写进{},不需要在外边组装一大坨元组!
python
for i, (item, count) in enumerate(pantry):
f_str = f'#{i+1}: {item.title():<10s} = {round(count)}'
print(f_str)
输出:
Plain
#1: Avocados = 1
#2: Bananas = 2
#3: Cherries = 15
如果一行太长,还可以利用 Python 相邻字符串拼接,把 f‑string 拆成多行,可读性拉满:
python
for i, (item, count) in enumerate(pantry):
print(f'#{i+1}: '
f'{item.title():<10s} = '
f'{round(count)}')
✨优势 2:格式参数也支持变量,告别硬编码
很多场景下小数点保留位数、对齐宽度是运行时才确定的变量。f‑string 支持嵌套大括号,动态控制格式,这是非常酷炫的特性!
python
places = 3
number = 1.23456
print(f"My number is {number:.{places}f}")
# My number is 1.235
⚡补充:简单聊一嘴性能
很多同学会关心速度,这里补充一个小测试代码:
python
import timeit
setup = """
key = "my_Var"
value = 1.234
"""
t_percent = timeit.timeit('"%-10s = %.2f" % (key, value)', setup=setup, number=200000)
t_format = timeit.timeit('"{:<10} = {:.2f}".format(key, value)', setup=setup, number=200000)
t_fstring = timeit.timeit('f"{key:<10} = {value:.2f}"', setup=setup, number=200000)
print(f"%操作符耗时:{t_percent:.3f}s")
print(f"format耗时:{t_format:.3f}s")
print(f"f‑string耗时:{t_fstring:.3f}s")
运行结果大致趋势:f‑string > % > format,f‑string 不仅写得清爽,性能也是三者最优。因为 f‑string 在语法解析阶段直接解析内部表达式,运行时开销很小。
📝总结与最佳实践
| 方案 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|
%百分号 |
简单,兼容老版本 Python | 顺序风险、预处理臃肿、字典模式大量重复抄写 | ⭐ |
str.format() |
支持索引、关键字、丰富格式语法 | 没有解决表达式臃肿、重复传参,性能偏弱 | ⭐⭐ |
| f‑string | 语法简洁,直接写表达式,支持嵌套格式,性能最优 | 仅 Python3.6 + 支持 | ⭐⭐⭐⭐⭐ |
💡开发建议
-
新项目 Python 版本大于等于 3.6,优先无脑选用 f‑string,日志打印、变量拼接、报表输出都用它。
-
弄懂
{:xxx}这套格式迷你语言,这套语法format、f‑string 通用。 -
维护老项目,如果是遗留
%代码不必强行重构;写新代码尽量不要再写%和.format()。 -
f‑string 多行拆分使用多个 f 字符串拼接,不要在单个
{}里面塞超级复杂巨长表达式,该抽辅助函数就抽函数。
📖《Effective Python》条款 4 原文要点回顾:
%C 风格格式化存在诸多缺陷,尽量规避;
str.format的格式迷你语言值得学习,但本身仍有不少短板,不推荐主力使用;f‑string 可以直接嵌入 Python 表达式,解决老式格式化绝大多数痛点,简洁强大,是现代 Python 首选。

如果你需要,我可以帮你把这篇博客再精简成适合掘金 / CSDN 发布的版本,生成配套标题列表和文末标签。要不要使用工作任务模式优化成完整可直接发布的博文?