小周那天接了个需求,要写一个通知模板系统。
产品经理的要求是:消息模板存在数据库里,运营可以随时改。用户下单后,系统读取模板,把用户名、订单号、金额这些变量填进去,生成最终消息发给用户。
小周心想,字符串格式化嘛,简单。他写了一段代码:
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就不再是坑,而是两个各有适用场景的工具。