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

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 同步视频

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

🧨 老古董: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+1item.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 + 支持 ⭐⭐⭐⭐⭐

💡开发建议

  1. 新项目 Python 版本大于等于 3.6,优先无脑选用 f‑string,日志打印、变量拼接、报表输出都用它。

  2. 弄懂{:xxx}这套格式迷你语言,这套语法format、f‑string 通用。

  3. 维护老项目,如果是遗留%代码不必强行重构;写新代码尽量不要再写%.format()

  4. f‑string 多行拆分使用多个 f 字符串拼接,不要在单个{}里面塞超级复杂巨长表达式,该抽辅助函数就抽函数。

📖《Effective Python》条款 4 原文要点回顾:

  1. %C 风格格式化存在诸多缺陷,尽量规避;

  2. str.format的格式迷你语言值得学习,但本身仍有不少短板,不推荐主力使用;

  3. f‑string 可以直接嵌入 Python 表达式,解决老式格式化绝大多数痛点,简洁强大,是现代 Python 首选。

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

相关推荐
Ramble_Naylor42 分钟前
只借不占:Rust 的引用与借用
开发语言·rust
FreeTinker1 小时前
Java文件服务器的技术选型与实现路径:从嵌入式工具到企业级系统
java·服务器·开发语言
2501_906565121 小时前
小创 · 智能助手
开发语言·c#
ynchyong1 小时前
Python 字符组合生成器:product 与 permutations 的实战对比
python·工具·字符串生产
benchmark_cc1 小时前
1000只ETF的5分钟K线如何批量获取?QuantDash分页策略与高性能Python实践
开发语言·人工智能·爬虫·python·算法·quantdash·量化数据源
Coodor1 小时前
如何在web浏览器使用js操作CPU卡
开发语言·前端·javascript·cpu卡
sel_91 小时前
【PEFT】参数高效微调(PEFT)技术详解:从原理到 LoRA/QLoRA 实战
人工智能·python·深度学习·算法·机器学习·参数高效微调
我星期八休息1 小时前
Linux I/O多路转接—epoll
java·linux·运维·服务器·开发语言·jvm·算法