Python的字符串格式化把我坑惨了,原来f-string和format的区别这么大

小周那天接了个需求,要写一个通知模板系统。

产品经理的要求是:消息模板存在数据库里,运营可以随时改。用户下单后,系统读取模板,把用户名、订单号、金额这些变量填进去,生成最终消息发给用户。

小周心想,字符串格式化嘛,简单。他写了一段代码:

ini 复制代码
template = get_template_from_db()  # 返回 "尊敬的{name},您的订单{order_id}已确认,金额{amount}元。"

name = "张三"
order_id = "ORD123456"
amount = 99.5

message = f"{template}"
print(message)

跑出来是这样:

复制代码
尊敬的{name},您的订单{order_id}已确认,金额{amount}元。

变量原封不动地留在字符串里,一个都没被替换。小周愣了:"我明明写了f前缀,为什么没生效?"

他盯着代码想了半天,终于意识到问题:f"{template}"的意思是"把变量template的值拼进来",而不是"把template当成一个模板去格式化"。f前缀作用在字面量字符串上,不是作用在变量上。变量里的花括号对f-string来说只是普通字符。

他换成format:

scss 复制代码
message = template.format(name=name, order_id=order_id, amount=amount)
print(message)

这次对了:

复制代码
尊敬的张三,您的订单ORD123456已确认,金额99.5元。

但小周又冒出一个疑问:既然format能处理动态模板,f-string处理不了,那平时写代码为什么大家都推荐f-string?两者到底有什么区别?

这个问题,正是很多人没搞清楚的地方。

先看一个最直观的对比

ini 复制代码
name = "Alice"
age = 30

# f-string
s1 = f"{name} is {age} years old"

# format
s2 = "{} is {} years old".format(name, age)
s3 = "{name} is {age} years old".format(name=name, age=age)

print(s1)  # Alice is 30 years old
print(s2)  # Alice is 30 years old
print(s3)  # Alice is 30 years old

三种写法结果一样。区别在于:f-string把变量直接写在花括号里,format把变量作为参数传进去。

用一个生活场景来理解。你要寄一份快递,地址和收件人信息要填在快递单上。

f-string的做法是:你填单子的时候,直接把地址和姓名写上去。单子填好了,信息就固定了。

format的做法是:你有一张空白模板单 ,上面写着"收件人:,地址:"。你先把模板准备好,等寄的时候再把信息填进去。

区别一目了然:f-string是"当场写好",format是"先备模板,后填内容"。

这个区别看起来只是语法风格,但它决定了两者适用的场景完全不同。

f-string:定义时求值,当场绑定

f-string是Python 3.6引入的语法。它的本质是字符串字面量,在代码执行到这一行时,花括号里的表达式会立刻被求值,结果嵌入字符串。

ini 复制代码
name = "Alice"
s = f"Hello, {name}"
print(s)  # Hello, Alice

name = "Bob"
print(s)  # Hello, Alice,不会变

s在创建的那一刻就把name的值"Alice"固定下来了。之后name再怎么变,s的内容不变。

这就是"定义时求值"。

f-string的花括号里可以放任何Python表达式:

python 复制代码
x = 10
y = 20

print(f"{x + y}")              # 30
print(f"{x * 2}")              # 20
print(f"{'hello'.upper()}")    # HELLO
print(f"{[i**2 for i in range(3)]}")  # [0, 1, 4]

它甚至能调用函数、访问属性、做条件判断:

python 复制代码
user = {"name": "Alice", "age": 30}
print(f"{user['name']} is {user['age']}")  # Alice is 30
print(f"{'成年' if user['age'] >= 18 else '未成年'}")

f-string的表达式在运行时立即求值,所以它能访问当前作用域的所有变量。这是它最大的优势:写起来直观,不需要把变量一个个列出来传参。

format:调用时求值,参数传入

format是字符串的方法。它接受位置参数或关键字参数,把字符串里的{}占位符替换成对应的值。

ini 复制代码
template = "{} is {} years old"
s = template.format("Alice", 30)
print(s)  # Alice is 30 years old

注意关键点:template本身是一个普通字符串,它的内容在定义时不会求值。{}只是占位符,真正替换发生在调用.format()的时候。

这就是"调用时求值"。

正因为如此,format可以处理运行时才拿到的字符串:

ini 复制代码
template = get_template_from_db()  # 从数据库读取,内容是 "{name} 您好"
message = template.format(name="张三")

模板是什么内容,代码写的时候不知道。只有运行时从数据库读出来,才知道。f-string做不到这一点,因为f-string的花括号是语法的一部分,必须在代码里写死。

format支持位置参数、关键字参数、索引访问、属性访问:

python 复制代码
# 位置参数
"{} {}".format("hello", "world")  # "hello world"

# 重复使用同一个参数
"{0} {0} {1}".format("a", "b")    # "a a b"

# 关键字参数
"{name} is {age}".format(name="Alice", age=30)

# 索引访问
"{0[0]} {0[1]}".format(["a", "b"])  # "a b"

# 属性访问
"{0.name}".format(user)  # 访问 user.name

format还支持格式规格,比如对齐、填充、精度:

lua 复制代码
"{:>10}".format("hi")      # "        hi",右对齐
"{:<10}".format("hi")      # "hi        ",左对齐
"{:^10}".format("hi")      # "    hi    ",居中
"{:.2f}".format(3.14159)   # "3.14",保留两位小数
"{:,}".format(1000000)     # "1,000,000",千分位

f-string也支持同样的格式规格,语法更简洁:

python 复制代码
f"{'hi':>10}"
f"{3.14159:.2f}"
f"{1000000:,}"

核心区别:什么时候求值

这是两者最本质的区别,也是小周踩坑的根源。

f-string在代码执行到那一行时求值。花括号里的变量必须是当前作用域里存在的,或者能通过表达式计算出来的。

format在调用.format()那一刻求值。占位符里的变量名只是字符串里的普通字符,和当前作用域没有任何关系,必须通过参数传进去。

用一个快递的比喻来总结:

f-string是"现场填单"。你手里有地址和姓名,当场写在快递单上。单子写好,信息就固定了。你不可能拿一张别人给你的空白单子说"帮我现场填一下",因为f-string的花括号是你代码里写死的。

format是"模板盖章"。你有一张模板单,上面有空格。你拿着模板单,把信息一个一个盖上去。模板单可以是别人给你的,可以是数据库里读出来的,可以是用户输入的。

坑一:用 f-string 处理动态模板

小周踩的就是这个坑。

ini 复制代码
template = "{name} 您好"
message = f"{template}"  # 输出 "{name} 您好",花括号没被替换

f"{template}"只是把template变量的值原样拼进来。template里的{name}对f-string来说只是普通字符,不是占位符。

正确的做法是用format:

ini 复制代码
message = template.format(name="张三")

或者用string.Template:

ini 复制代码
from string import Template

template = Template("$name 您好")
message = template.substitute(name="张三")

string.Template用$作为占位符,避免了花括号和JSON、代码块冲突的问题。但它的功能比format弱,不支持格式规格。

坑二:在 f-string 里用花括号表示字面量

f-string用花括号表示表达式,如果你想输出字面的花括号,必须双写:

python 复制代码
print(f"{{hello}}")  # {hello}
print(f"{{{1 + 2}}}")  # {3}

单写一个花括号会报错:

python 复制代码
print(f"{hello}")  # NameError: name 'hello' is not defined

这个规则在处理JSON、CSS、代码片段时特别容易踩坑。比如你想用f-string生成一段JSON:

ini 复制代码
data = {"name": "Alice"}
json_str = f'{{"name": "{data["name"]}"}}'
print(json_str)  # {"name": "Alice"}

外层双花括号表示字面量,内层单花括号表示表达式。写起来很绕,容易出错。

用format就没有这个问题,因为format的花括号只出现在模板字符串里,不需要转义:

css 复制代码
json_str = '{{"name": "{}"}}'.format(data["name"])

但format同样需要双写花括号来表示字面量。所以两者在这一点上都不方便。真正要生成JSON,应该用json.dumps(),而不是手动拼字符串。

坑三:f-string 不能跨行

f-string在Python 3.12之前不支持跨行表达式(除非用括号包裹)。如果你在f-string里写一个很长的表达式,换行会报错:

ini 复制代码
# Python 3.11 及之前会报错
s = f"{some_function(arg1, arg2,
                     arg3)}"

需要写成一行,或者用括号包裹:

ini 复制代码
s = f"{some_function(arg1, arg2, arg3)}"

Python 3.12之后支持了f-string内换行,但为了兼容性,很多项目还是避免在f-string里写太长的表达式。

format没有这个限制,因为表达式在参数里,可以正常换行。

坑四:f-string 中的引号嵌套

f-string的花括号里可以写表达式,表达式里可能包含字符串,字符串可能需要引号。引号嵌套容易出问题:

ini 复制代码
# Python 3.11 及之前,f-string 内外引号不能相同
s = f"{'hello'}"  # 可以,内层单引号,外层双引号
s = f'{"hello"}'  # 可以,内层双引号,外层单引号
s = f"{"hello"}"  # SyntaxError,内外都是双引号

Python 3.12之后放宽了这个限制,但为了兼容旧版本,最好避免内外引号相同。

format没有这个问题,因为参数在括号外面,引号可以自由使用。

坑五:format 的性能不如 f-string

在大多数场景下,f-string比format快。因为f-string在编译时就转换成了字节码,运行时直接拼接。而format需要解析模板字符串,查找占位符,再替换。

python 复制代码
import timeit

name = "Alice"
age = 30

t1 = timeit.timeit(lambda: f"{name} is {age}", number=1000000)
t2 = timeit.timeit(lambda: "{} is {}".format(name, age), number=1000000)

print(f"f-string: {t1:.4f}")
print(f"format: {t2:.4f}")

在我的机器上,f-string大约0.15秒,format大约0.35秒。f-string快一倍多。

但要注意:如果你需要反复使用同一个模板,format可以提前编译:

ini 复制代码
template = "{} is {}".format  # 不编译
template = string.Formatter().parse(...)  # 手动编译,太麻烦

实际上,Python的format没有提供公开的预编译接口。如果你的模板是固定的,f-string在性能上占优。如果模板是动态的,只能用format,性能差异就不重要了。

什么时候用 f-string,什么时候用 format?

判断标准很清晰:

模板是代码里写死的,变量在当前作用域:用f-string。可读性最好,性能最好。

模板是运行时才拿到的(数据库、配置文件、用户输入) :用format。f-string处理不了动态模板。

需要重复使用同一个模板 :用format。把模板存起来,每次调用.format()。

需要把变量名和值分离:用format。比如日志系统里,模板和参数分开传递。

Python 3.6以下 :只能用format或%。但现在已经很少有这样的环境了。

一个容易忽略的点:format 可以访问对象属性

format的占位符支持属性访问和索引访问,这在某些场景下很方便:

python 复制代码
class User:
    def __init__(self, name):
        self.name = name

user = User("Alice")
print("{0.name}".format(user))  # Alice

data = {"key": "value"}
print("{0[key]}".format(data))  # value

f-string也支持属性访问,但语法更自然:

python 复制代码
print(f"{user.name}")  # Alice
print(f"{data['key']}")  # value

两者都能做到,但f-string写起来更直观。

回到小周的故事

小周最终的系统是这样的:

模板存在数据库里,运营可以随时改。代码读取模板,用format填充变量:

ini 复制代码
template = get_template_from_db()
message = template.format(name=user.name, order_id=order.id, amount=order.amount)

但他在一些固定格式的场景,比如日志输出、错误消息,还是用f-string:

python 复制代码
logger.info(f"用户 {user.name} 下单成功,订单号 {order.id}")

他后来在团队文档里写了一条:"模板是代码里写死的,用f-string。模板是运行时拿到的,用format。f-string是字面量,花括号在编译时就被解析了;format是方法,花括号在调用时才被替换。别用f-string去处理动态模板,它做不到。"

这个区分看起来简单,但真到写代码的时候,很多人会下意识写f"{template}",以为f前缀能"激活"变量里的占位符。实际上f前缀只对字面量字符串生效,变量里的花括号它管不着。

记住那个快递的比喻:f-string是现场填单,format是模板盖章。单子是你自己写的,用f-string;单子是别人给的,用format。

字符串格式化本身不难,难的是想清楚"这个模板是谁写的、什么时候能拿到"。想清楚了,f-string和format就不再是坑,而是两个各有适用场景的工具。

相关推荐
雾行0x001 小时前
精排:推荐系统如何给内容打分?从多目标预估到分数融合
后端·架构
今天早睡1 小时前
Checkpointer 和 thread_id 如何让状态跨调用延续
后端
用户423341246191 小时前
Redis 缓存穿透、击穿、雪崩,一次性讲清楚
后端
姜鱼问生1 小时前
2G 内存跑 5 个容器频频 OOM?一招 Swap 抢救你的 2核2G 小破服务器
后端
jose1 小时前
存量 Eureka 平滑接入 Nacos:内嵌 Sync 与双注册 SDK 双路线对比
后端·架构
lazy H2 小时前
【计算机网络】第 2 篇:一条数据如何穿过网络?彻底理解数据封装与解封装
网络·经验分享·后端·学习·计算机网络
我命由我123452 小时前
软件工程概念 - 可插拔性
java·后端·学习·java-ee·系统架构·软件工程·学习方法
Bazingga3 小时前
RAG路由全景:一个问题走到岔路口,谁来指路
后端
云边有个稻草人3 小时前
从日志到落库:数据集成产品 KFS 如何打通异构数据库实时同步链路
后端