欢迎光临
我们一直在努力

Effective Python 条款4:字符串格式化大乱斗

Effective Python 条款4:字符串格式化大乱斗

  • Bilibili 同步视频
  • 🧨 老古董:C 风格 % 格式化操作符
    • 坑 1:顺序、类型不匹配,直接运行时报错
    • 坑 2:填充前要预处理变量,代码疯狂膨胀
    • 坑 3:同一个值多处使用,必须重复传参
    • 坑 4:字典模式,键名反复抄写,代码冗余爆炸
  • 🛠️ 改良方案 str.format (),理想很丰满现实骨感
    • ⚠️但是!format 没有根治老问题
  • 🏆王者登场 f‑string,Python3.6 + 的终极答案🎉
    • ✨ 优势 1:大括号内直接运行表达式,预处理一步到位
    • ✨优势 2:格式参数也支持变量,告别硬编码
    • ⚡补充:简单聊一嘴性能
  • 📝总结与最佳实践
    • 💡开发建议

摘要:字符串格式化是 Python 开发中绕不开的基本功,打印日志、拼接输出、组装报文处处都要用到它。Python 一共提供了 4 套内置格式化方案,但并不是每一种都值得我们在项目中使用。今天我们就扒一扒老式%、str.format()的各种坑,搞懂为什么f‑string 才是现代 Python 的最优解。

不知道你有没有踩过字符串格式化的坑😵‍💫:明明代码看着没问题,一运行直接抛出TypeError;改一个变量,模板里好几处都要同步修改;格式化代码越写越长,换行拆得七零八落,读起来像一团乱麻。

很多 Pythoner 写代码,上来就直接%百分号格式化,毕竟它源自 C 语言printf,上手简单。但好用不代表没有隐患,《Effective Python》条款 4就专门剖析了 Python 的几种字符串格式化方案,我们今天结合实战案例,聊聊各个方案的优缺点。

Bilibili 同步视频

Effective Python 条款4:字符串格式化大乱斗

🧨 老古董:C 风格 % 格式化操作符

%是 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:顺序、类型不匹配,直接运行时报错

元组传参,顺序必须和模板内格式符一一对应,一旦顺序颠倒,类型对不上,直接抛异常。

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,不再依赖参数顺序:

key = "my_Var"
value = 1.234

fmt = "%(key)-10s = %(value).2f" % {"key": key, "value": value}
print(fmt)

这样确实解决顺序错乱的问题,但是会引出新的麻烦,我们往下看。

坑 2:填充前要预处理变量,代码疯狂膨胀

业务场景中,我们经常要对变量做预处理:比如数字四舍五入、字符串首字母大写。 拿食材列表打印举例:

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) # 数值四舍五入
))

输出:

#1: Avocados = 1
#2: Bananas = 2
#3: Cherries = 15

仅仅 3 个简单处理,%右侧元组就必须拆成多行。如果业务逻辑更复杂,预处理更多,整段格式化代码会变得臃肿难读。

坑 3:同一个值多处使用,必须重复传参

模板里同一个变量要出现多次,元组里面就得重复写几遍变量:

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 次。

soup = "lentil"
formatted = "Today's soup is %(soup)s." % {"soup": soup}
print(formatted)

这里soup这个名字写了三次!业务模板一旦变长,体验会更难受:

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()函数可以单独处理单个变量,支持千位分隔符、居中对齐等丰富的格式迷你语言:

a = 1234.5678
print(format(a, ",.2f")) # 千位分隔,保留两位小数 →1,234.57

b = "my string"
print(f"*{format(b, '^20s')}*")
# * my string *

字符串的.format()方法,可以在大括号内写索引,解除参数顺序绑定:

key = "my_Var"
value = 1.234
# {1}取第二个参数,{0}取第一个参数,不关心模板内顺序
out = "{1} = {0}".format(key, value)
print(out) #1.234 = my_Var

同一个索引可以多次复用,解决变量重复传入的痛点:

name = "Max"
out = "{0} loves food. See {0} cook.".format(name)
print(out)

大括号内支持冒号书写格式规则,还支持!r获取 repr 字符串,也支持访问字典 key、列表下标:

menu = {"oyster": "kumamoto"}
out = "First letter is {menu[oyster][0]!r}".format(menu=menu)
print(out) # First letter is 'k'

⚠️但是!format 没有根治老问题

虽然它修复了顺序错乱、重复传参的问题,但预处理变量时代码臃肿、键名重复抄写两大痛点依旧存在。

对比下面两段等价代码,format写法并没有比老式%清爽多少:

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。

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 表达式,可以直接访问当前作用域所有变量,不需要额外传参、不需要构造元组、不需要写字典。

基础示例:

key = "my_Var"
value = 1.234
formatted = f"{key} = {value}"
print(formatted) # my_Var = 1.234

原来那套强大的格式迷你语言完全兼容,搭配!r也不在话下:

formatted = f"{key!r:<10} = {value:.2f}"
print(formatted) # 'my_Var' = 1.23

我们把四种写法放在一起横向对比,高下立判:

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 中,直接把表达式写进{},不需要在外边组装一大坨元组!

for i, (item, count) in enumerate(pantry):
f_str = f'#{i+1}: {item.title():<10s} = {round(count)}'
print(f_str)

输出:

#1: Avocados = 1
#2: Bananas = 2
#3: Cherries = 15

如果一行太长,还可以利用 Python 相邻字符串拼接,把 f‑string 拆成多行,可读性拉满:

for i, (item, count) in enumerate(pantry):
print(f'#{i+1}: '
f'{item.title():<10s} = '
f'{round(count)}')

✨优势 2:格式参数也支持变量,告别硬编码

很多场景下小数点保留位数、对齐宽度是运行时才确定的变量。f‑string 支持嵌套大括号,动态控制格式,这是非常酷炫的特性!

places = 3
number = 1.23456
print(f"My number is {number:.{places}f}")
# My number is 1.235

⚡补充:简单聊一嘴性能

很多同学会关心速度,这里补充一个小测试代码:

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 首选。

  •  Effective Python 条款4:字符串格式化大乱斗

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

    赞(0)
    未经允许不得转载:171主机测评 » Effective Python 条款4:字符串格式化大乱斗
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址