Python f-string 前面的 f 到底编译成了什么?本文讲清完整的格式规范迷你语言、__format__,以及 f-string 不该用的三种场合。所有输出都在 Python 3.12 上实际运行过。
大多数 Python 开发者每天都在用 f-string,却只懂它五分之一的本事。
这不是批评。你从一个例子里学会了 f"{name}",它能用,也从来没有理由再往下挖。直到有一天,你要在一张对齐的表格里把数字保留两位小数,才发现那个冒号有你从没学过的用法。
本文讲的就是剩下的五分之四:f 到底做了什么,完整的格式规范语言,怎么让自己的类支持它,以及 f-string 不该用的三种场合。
这里的所有代码都能在普通的 Python 交互式环境里运行,不用装任何东西。下面每一段输出都在 Python 3.12.3 上跑过,是从真实会话里粘贴过来的。
f 是给编译器的指令
先看一个区别,它能解释后面的一切。运行这两行:
f"{undefined_name}"
"{undefined_name}"
第一行抛出 NameError。第二行只是一个碰巧含有花括号的字符串。
f-string 不是加了功能的字符串,而是长得像字符串的另一种东西。"{x}" 以文本形式存储。f"{x}" 根本不会存储在任何地方:Python 把它编译成一组指令,这一行运行时再用这些指令拼出字符串。
这些指令你可以亲眼看到:
import dis
def greet(name, n):
return f"{name} has {n}"
dis.dis(greet)
LOAD_FAST 0 (name)
FORMAT_VALUE 0
LOAD_CONST 1 (' has ')
LOAD_FAST 1 (n)
FORMAT_VALUE 0
BUILD_STRING 3
RETURN_VALUE
加载 name,格式化;加载字面量 ' has ';加载 n,格式化;最后把三段拼成一个字符串。没有调用 .format(),也没有在哪里存着模板。
由此可以推出三点,每一点迟早都会让人栽跟头:
- 花括号里的拼写错误在导入时就是
SyntaxError,不会等到运行时才冒出来。 - 值在这一行运行的那一刻读取。你没法现在构造一个 f-string、以后再填值,因为根本没有可填的东西。
- 它很快,因为没有模板要解析。文末我们会实测。
f-string 不是字符串,而是一段生成字符串的小程序。
只要是表达式,就能放进花括号
花括号里放的是表达式,也就是能产生值的东西;不能放语句,也就是做某件事的东西。规则就这一条。
>>> f"{2 + 2}"
'4'
>>> f"{'Ada'.upper()}"
'ADA'
>>> f"{[i * i for i in range(4)]}"
'[0, 1, 4, 9]'
一整个列表推导式在字符串里跑了一遍。这里没有什么 f-string 专属的子语言,就是 Python。只要能在交互式环境里敲出来并得到一个值,就能放进花括号。
f"{x = 5}" 不行,因为赋值是语句。f"{if x: 1}" 也不行。
这份自由是真的,也是写出没人看得懂的代码的捷径:
# don't
f"Top: {sorted(users, key=lambda u: -u['score'])[0]['name']}"
# do
top = max(users, key=lambda u: u["score"])
f"Top: {top['name']}"
输出一样,而且第二种写法只排一遍,不是两遍。经得起考验的准则是:花括号里放查找或简短的调用,逻辑单独写一行。如果你已经在数括号了,说明写过头了。
花括号、引号和反斜杠
人们实际遇到的 f-string 错误,大多出在三条规则上。三条都会在导入时大声报错,这算是好结果。
字面花括号要双写。 {{ 得到 {,}} 得到 }:
>>> n = 7
>>> f"{{count: {n}}}"
'{count: 7}'
成对地读:字面左花括号,真正的替换,字面右花括号。不双写的话,f"{status: ok}" 会把 status 当成变量,把冒号后面的内容当成格式规范,于是你得到:
ValueError: Invalid format specifier ' ok' for object of type 'str'
引号要交替用。 f-string 外面用一种引号,花括号里用另一种:
user = {"name": "Ada"}
f"{user['name']}"
Python 3.12 取消了这个限制,所以在 3.12 上 f"{user["name"]}" 是合法的。在 3.11 及更早版本上,它是 SyntaxError。文末还会细说。
花括号里别放反斜杠。 3.12 之前,f"{'\n'.join(names)}" 会报错。把反斜杠挪到变量里:
newline = "\n"
f"{newline.join(names)}"
这在所有版本上都能用。
冒号后面是另一门语言
>>> value = 3.14159
>>> f"{value:.2f}"
'3.14'
.2f 不是 Python。你在交互式环境里敲它,什么也得不到。
一对花括号最多有三部分,只有第一部分是 Python:
{expression!conversion:format_spec}
| 部分 | 示例 | 是什么 |
|---|---|---|
| 表达式(expression) | value |
Python |
| 转换(conversion) | !r |
s、r、a 三者之一 |
| 格式规范(format spec) | :.2f |
另一门语言 |
冒号后面的一切是格式规范迷你语言。它借自 .format(),.format() 借自 % 格式化,% 格式化又借自 C。这段历史解释了它为什么看起来像乱码:它很老,而且当初就是为了频繁输入而设计得很短。
下面是完整语法。每一部分都可选,顺序固定:
[[fill]align][sign][z][#][0][width][grouping][.precision][type]
你不用背下来。你只需要知道顺序是固定的,这样就能把一个规范拆开来读,而不是靠猜:
>>> f"{1234.5678:>10,.2f}"
' 1,234.57'
| 片段 | 部分 | 含义 |
|---|---|---|
> |
对齐(align) | 右对齐 |
10 |
宽度(width) | 字段宽 10 个字符 |
, |
分组(grouping) | 千位之间加逗号 |
.2 |
精度(precision) | 两位小数 |
f |
类型(type) | 定点小数 |
下面逐个来看。
宽度、对齐和填充
只写宽度,就得到一列:
>>> f"|{'Ada':10}|"
'|Ada |'
>>> f"|{7:10}|"
'| 7|'
注意,字符串默认左对齐,数字默认右对齐。这是有意为之:文字从左往右读,数字按最后一位对齐。但它也很容易让你吃惊。对齐方式要紧的时候,就明确写出来:
>>> n = 7
>>> f"|{n:<8}|{n:>8}|{n:^8}|"
'|7 | 7| 7 |'
< 左对齐,> 右对齐,^ 居中。还有第四种 =,只用于数字:它把填充放在符号和数字之间,正是账本需要的样子:
>>> f"{7:=+9}"
'+ 7'
对齐符号前面的任意单个字符就是填充字符:
>>> f"{7:*^9}"
'****7****'
>>> f"{'menu':.<20}"
'menu................'
顺序永远是先填充,后对齐。*^9 的意思是“用星号填充,居中,宽度 9”。^*9 没有这种写法。
有个误解值得澄清:宽度是最小值,从来不是最大值。
>>> f"|{'a very long name':10}|"
'|a very long name|'
这一列被撑破了。如果需要硬截断,那是精度的事,后面会讲。
给人读的数字
>>> revenue = 1234567.891
>>> f"{revenue}"
'1234567.891'
>>> f"{revenue:,.2f}"
'1,234,567.89'
读第一个数,你得数位数。, 负责千位分组,.2f 固定小数位数。这是两个独立的功能,可以单独使用。
_ 做同样的事,只是用下划线:
>>> f"{1234567:_}"
'1_234_567'
给人读的用 ,;输出要写回 Python 源码或配置文件时用 _。Python 能把 1_234_567 当成数字读,却读不了 1,234,567。
% 会乘以 100 并加上百分号:
>>> f"{0.4567:.1%}"
'45.7%'
留意传进来的值。如果上游已经乘过 100,你会得到 4567.0%。这种错误很显眼,属于好的那一类。
还有一个人人都当成 bug 来报的:
>>> f"{2.675:.2f}"
'2.67'
这是对的。2.675 在二进制浮点数里并不精确等于 2.675,而是略小一点点,所以向下舍入了。格式化只是如实反映了它拿到的值:
>>> f"{0.1 + 0.2}"
'0.30000000000000004'
如果你需要按金钱的规矩来算钱,就用 decimal.Decimal。f-string 用同一套规范语言格式化它,代码其他地方都不用改:
>>> from decimal import Decimal
>>> f"{Decimal('0.1') + Decimal('0.2')}"
'0.3'
符号和补零
符号有三种选项,紧跟在对齐之后:
>>> f"{5:+d} {-5:+d}" # + : always show a sign
'+5 -5'
>>> f"{5:-d} {-5:-d}" # - : negatives only (the default)
'5 -5'
>>> f"{5: d} {-5: d}" # space : a space where the plus would be
' 5 -5'
第三种是个不起眼的小技巧。正数前面留一个空格,列照样对齐,又不用把 + 硬塞给读者。
宽度前面加 0 表示用零填充,而且零填在符号后面:
>>> f"{7:03d}"
'007'
>>> f"{-7:04d}"
'-007'
>>> f"{-7:0>4}" # plain fill, for comparison
'00-7'
最后那个对数字来说是错的,这正是 0 这个简写存在的原因。凡是要按文本排序的东西,补零都是你想要的:
>>> for i in [1, 9, 10, 99]:
... print(f"INV-{i:05d}")
INV-00001
INV-00009
INV-00010
INV-00099
把这些当字符串排序,结果就是数字顺序。不补零就不是。
表示类型
整数的表示类型各占一个字符:
>>> f"{255:b} {255:o} {255:x} {255:X}"
'11111111 377 ff FF'
加上 #,就会带上 Python 自己会写的前缀。如果输出还要被程序读回去,这一点很重要:
>>> f"{255:#x} {255:#b} {255:#o}"
'0xff 0b11111111 0o377'
再配合补零,就能查看字节:
>>> data = bytes([0, 15, 255, 16])
>>> " ".join(f"{b:02x}" for b in data)
'00 0f ff 10'
浮点数方面,f 是定点表示,e 是科学计数法,g 在两者之间自动选择,并去掉末尾的零:
>>> f"{0.000012345:g}"
'1.2345e-05'
>>> f"{1234.5:g}"
'1234.5'
数量级变化大时用 g。想让每一行小数位数相同时用 f,表格几乎总是要这样。
精度用在字符串上意思就不同了:它会截断。
>>> f"{'a very long name':.6}"
'a very'
再配上相同的宽度,就得到一列永远不会溢出的内容:
>>> for name in ["Ada", "a very long name indeed"]:
... print(f"|{name:<10.10}|")
|Ada |
|a very lon|
运行时构造规范
规范本身也可以包含替换字段。Python 先解析这些字段,拼出规范,再应用它:
>>> width, places = 12, 3
>>> f"{1234.5678:{width}.{places}f}"
' 1234.568'
表格就是这样根据数据定宽的:
>>> rows = [("Ada", 91), ("Grace", 88), ("Alan Turing", 95)]
>>> w = max(len(name) for name, _ in rows)
>>> for name, score in rows:
... print(f"{name:<{w}} {score:>3}")
Ada 91
Grace 88
Alan Turing 95
数据变了,照样对得齐。格式字符串里没有魔法数字,也不用再跑一遍去修对齐。
嵌套只能有一层。实际用下来,这从来不是问题。
=:从此不用再手写调试打印
>>> user_count = 42
>>> f"{user_count=}"
'user_count=42'
变量名你只敲了一次。Python 3.8 加入这个功能正是为此:大家调试时每个变量都要敲两遍,而且变量一改名,标签有一半时候就对不上了。
它适用于任何表达式,左边的文本和你敲的一模一样,连空格都保留:
>>> items = [1, 2, 3]
>>> f"{len(items)=}"
'len(items)=3'
>>> x = 42
>>> f"{x = }"
'x = 42'
它还能和规范叠加:
>>> price = 1234.5678
>>> f"{price=:>12,.2f}"
'price= 1,234.57'
有一点会让人意外:单独的 = 用的是 repr,不是 str。
>>> name = "Ada"
>>> f"{name=}"
"name='Ada'"
看那对引号。调试时这是正确的默认行为,也正好引出下一个话题。
!r:一个字符,修好你的错误信息
每个 Python 对象都能生成两种字符串,给两类读者看。str(obj) 给普通人看。repr(obj) 给开发者看:没有歧义,最好能直接粘回 Python 里用。
>>> for v in ["Ada", "", " Ada ", None]:
... print(f"{v!r}")
'Ada'
''
' Ada '
None
每一个都能分辨出来。不加 !r 的话,其中两个打印出来就像什么都没有。
所以它在错误信息里很重要:
raise ValueError(f"bad status: {status}") # 'bad status: ' — was it empty? None? a space?
raise ValueError(f"bad status: {status!r}") # "bad status: ''" — question answered
把它养成习惯:只要错误信息说的是某个值不对,就用 !r。只多一个字符,却消除了歧义,别人不用为了弄清那个值到底是什么而把失败重跑一遍。
还有 !a,它是把非 ASCII 字符转义之后的 repr:
>>> f"{'café'!a}"
"'caf\\xe9'"
大概一年用上一次:下游处理不了非 ASCII,你需要找出是哪个字符在捣乱。!s 也存在,而且是默认值,所以你永远不用写它。
__format__:解释以上一切的那一层
下面是你写过的每个 f-string 底下的机制。Python 格式化 {value:spec} 时,调用的是:
type(value).__format__(value, spec)
就这样。规范作为普通字符串原样传进去,由对象决定怎么处理。你可以看着它传进来:
class Spy:
def __format__(self, spec):
return f"spec={spec!r}"
>>> f"{Spy()}"
"spec=''"
>>> f"{Spy():>10,.2f}"
"spec='>10,.2f'"
并没有一个中央格式化引擎。int、float、str、Decimal 和 datetime 各自实现了自己的一套,所以它们都认得 ,.2f。
你的类继承来的是 object.__format__,它接受空规范并返回 str(self)。传别的规范进去,它就报错:
TypeError: unsupported format string passed to Point.__format__
要修好它,需要写三个方法,而最关键的那个什么都不解析:
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __str__(self):
return f"({self.x}, {self.y})"
def __repr__(self):
return f"Point({self.x!r}, {self.y!r})"
def __format__(self, spec):
if not spec:
return str(self)
return f"({self.x:{spec}}, {self.y:{spec}})"
它把规范转交给数字,而数字早就知道该怎么处理。两行代码,Point 就支持了整套规范语言:
>>> p = Point(3.14159, 2.71828)
>>> f"{p:.2f}"
'(3.14, 2.72)'
>>> f"{p:>8.1f}"
'( 3.1, 2.7)'
诀窍全在这次委托。如果你的对象包装的是数字,就把规范交给数字。
如果你只需要 f"{p}",定义 __str__ 和 __repr__ 就够了,不用写 __format__:继承来的那个在规范为空时会退回到 str。数据类(dataclass)会替你生成 __repr__,但不会生成 __format__,所以在数据类上用规范照样会失败。
f-string 自己不做格式化。它把规范交给对象,活由对象来干。
日期用的是完全不同的规范,而且有充分的理由
>>> from datetime import datetime
>>> now = datetime(2026, 9, 2, 14, 5, 9)
>>> f"{now:%Y-%m-%d %H:%M}"
'2026-09-02 14:05'
>>> f"{now:%A, %d %B %Y}"
'Wednesday, 02 September 2026'
这跟迷你语言毫无相似之处,原因上一节已经讲了:datetime.__format__ 完全不理会迷你语言,直接把规范交给 strftime。所以整套 strftime 代码都能在 f-string 里用。
你真正会用到的代码有:%Y 年,%m 月,%d 日,%H 时,%M 分,%S 秒,%B 月份名,%A 星期几,%p 上午/下午。注意 %M 是分钟,%m 是月份。大小写很重要,写错了会得到一个看起来很合理的错误日期。
最实用的好处是能排序的文件名:
>>> f"report-{now:%Y%m%d-%H%M}.csv"
'report-20260902-1405.csv'
timedelta 没有重写 __format__,所以规范语言对时间间隔不起作用。把数字取出来,格式化这些数字:
>>> from datetime import timedelta
>>> d = timedelta(hours=2, minutes=5)
>>> total = int(d.total_seconds())
>>> f"{total // 3600}h {total % 3600 // 60:02d}m"
'2h 05m'
只要对象不认识某个规范,通用的做法就是这样。
长字符串,以及一个不报错的 bug
相邻的字符串字面量在编译时就会拼接,所以这是拆分长行最省事的办法:
message = (
f"Hello {name}, "
f"your order {order_id} shipped "
f"and should arrive by {eta}."
)
每一段都要有自己的 f。漏掉一个,什么都不会崩:
>>> a = 1
>>> (f"value {a} "
... "and {a} again")
'value 1 and {a} again'
仔细看这段输出。第二个 {a} 作为字面文本原样出来了。这是最常见的静默 f-string bug:没有报错,只有一个错误的字符串,它可能流转很远才有人发现。
还要留意拼接处的空格。统一把空格放在每行末尾,就不会再丢了。
真正需要多行输出时,用三引号。如果源码缩进在函数里面,还要加上 textwrap.dedent,否则每一行都会带着缩进出来:
import textwrap
return textwrap.dedent(f"""
Order {order_id}
Customer: {name}
Total: {total:,.2f}
""")
f-string 不该用的三种场合
f-string 在写下它的地方求值,只求一次,不留下任何东西。凡是需要“现在定好形状,以后再填值”的活,都不归 f-string 管。这样的活有三类。
可复用的模板
template = "Hello {name}, you scored {score}"
template.format(name="Ada", score=91)
字符串始终是数据。你可以把它放进配置文件,从数据库里读出来,或者让用户提供。这些 f-string 一样都做不到。
同样的原因也排除了翻译:提取工具的原理是从源码里抽出字面字符串,而 f-string 没有字面量可抽。等它存在的时候,值已经填好了。
日志
这个看起来人畜无害。运行一下,盯着计数器:
import logging
logging.basicConfig(level=logging.WARNING)
log = logging.getLogger("demo")
calls = {"n": 0}
class Record:
def __str__(self):
calls["n"] += 1
return "the expensive text form"
r = Record()
log.debug(f"record: {r}")
print("after f-string debug:", calls["n"])
log.debug("record: %s", r)
print("after %s debug: ", calls["n"])
after f-string debug: 1
after %s debug: 1
日志级别是 WARNING,所以两行日志都被丢弃了。f-string 那行还是调用了 __str__。%s 那行没有:logging 把模板和值分开存放,只有某个处理器真的需要这条消息时才做替换。
纯速度上的差距没有人们说的那么大。20 万次被抑制的调试调用,在同一台机器上跑五次取中位数:
f-string 0.052s
%s 0.033s
每次调用大约相差十分之一微秒。真正要紧的是另一种开销:如果值是一行数据库记录,或者某个 __repr__ 要遍历整个结构的对象,那你每次都在做这份工作,然后把结果扔掉。
经得起考验的规则是:库代码和在循环里打日志的地方用 %s;值很廉价的应用代码用 f-string。两种都说得过去,不一致才说不过去。大多数 linter 都有针对这一点的规则,打开它,争论就结束了。如果两者都想要,就给开销大的情况加个保护:
if log.isEnabledFor(logging.DEBUG):
log.debug(f"record: {expensive_summary(record)}")
SQL
建一张表,用 f-string 查询:
import sqlite3
con = sqlite3.connect(":memory:")
con.execute("create table users(name text, role text)")
con.executemany("insert into users values (?,?)",
[("Ada", "admin"), ("Grace", "user"), ("Alan", "user")])
name = "Ada"
con.execute(f"select * from users where name = '{name}'").fetchall()
[('Ada', 'admin')]
能用。现在改一行:
name = "x' OR '1'='1"
con.execute(f"select * from users where name = '{name}'").fetchall()
[('Ada', 'admin'), ('Grace', 'user'), ('Alan', 'user')]
所有行都出来了。把查询打印出来,一眼就明白:
select * from users where name = 'x' OR '1'='1'
值里带了一个引号。这个引号闭合了查询正在拼的字符串,于是它后面的所有内容都成了查询的一部分,而不是值的一部分。你没有写 OR,是数据给你加上的。
修复方法是交给数据库驱动来处理:
>>> con.execute("select * from users where name = ?", (name,)).fetchall()
[]
没有返回任何行,这是对的:没有哪个用户叫这么古怪的名字。? 是占位符,不是字符串替换。数据库把查询和值当作两样东西分别接收,所以值永远不会被当成 SQL 解析,也就没有什么需要转义的。
不同驱动的写法不同:sqlite3 用 ?,psycopg 和 MySQL 用 %s,命名参数用 :name。但原理完全一样。
SQL 旁边出现 f-string 并不一定就是错的。值必须用参数传;标识符却不行,因为没有哪个数据库允许把表名或列名参数化。所以如果需要动态的列名,f-string 就是实现手段,安全则要你自己保证:
SORTABLE = {"name", "created_at", "score"}
def query(sort_by):
if sort_by not in SORTABLE:
raise ValueError(f"cannot sort by {sort_by!r}")
return f"select * from users order by {sort_by}"
起作用的是白名单。千万不要靠自己转义引号来做清理,这场游戏你迟早会输。
这其实不只关乎 SQL。只要你用另一门语言里不可信的文本来拼一门语言,就会犯同样的错:HTML、shell 命令、路径、正则表达式。本该是数据的文本,被当成了指令来读。
值用参数传,标识符用白名单。永远不要把用户输入格式化进查询里。
不算限制的那一条
人们常把“用户输入”也列进这张清单。它不属于这里:
f"Hello {user_name}" # fine
user_name 是被格式化的值,不是被执行的代码。危险在另一个方向:让用户提供模板。对不可信的模板调用 .format(),可能会顺着属性一路访问,碰到你本不打算暴露的东西。f-string 根本没法这样用,因为运行时没有模板可以交出去。就这一点而言,f-string 反倒是更安全的工具。
Python 3.12 改了什么
d = {"key": "value"}
f"{d["key"]}"
在 3.12 及以后的版本上,它打印 value。在 3.11 及更早版本上,它是 SyntaxError。对自己的代码下结论之前,先用 python3 -VV 查一下版本。
那些限制之所以存在,是因为 f-string 以前不经过 Python 真正的解析器。编译器先把字符串抽出来,对花括号之间的部分做文本处理,再单独交给解析器。规则让人觉得武断,是因为它们本来就是实现方式的副作用,而不是谁拍板定下的。PEP 701 重写了 f-string,让它走正常的解析器,这些限制也就随之消失了。
以下写法现在合法,但仅限 3.12 及以后:
f"{d["key"]}" # reusing the same quote
f"{"\n".join(names)}" # backslashes in the expression
f"{f"{f"{d["key"]}"}"}" # nesting as deep as you like, and please don't
total = f"{
sum(item['price'] for item in order) # multiple lines, and comments
:,.2f
}"
错误信息也变好了,因为解析器现在知道自己解析到了哪里。
这是兼容性问题,不是风格问题,而且只取决于一个问题:你的代码最低要跑在哪个 Python 版本上?如果运行环境由你掌控,而且是 3.12 或更高,在读起来更顺的地方就复用同一种引号。如果你写的是库,或者代码要跑在你掌控不了的环境里,就继续交替使用引号:它在包括 3.12 在内的所有版本上都能用,也没有任何不如人的地方。
速度,以及为什么它是最不值一提的理由
自己测一下,大约一秒钟:
import timeit
setup = "name='Ada'; n=7"
for label, stmt in [
("f-string", "f'{name} has {n}'"),
("format", "'{} has {}'.format(name, n)"),
("percent", "'%s has %s' % (name, n)"),
("concat", "name + ' has ' + str(n)"),
]:
t = timeit.timeit(stmt, setup=setup, number=1_000_000)
print(f"{label:9} {t:.3f}s")
跑一次,你会得到一个结果。跑七次,你会得到更可信的结果。在一台普通笔记本上用 Python 3.12,每次一百万次操作,重复七次:
| 最快 | 中位数 | 最慢 | |
|---|---|---|---|
| f-string | 0.133s | 0.149s | 0.230s |
.format() |
0.199s | 0.212s | 0.278s |
% |
0.145s | 0.161s | 0.176s |
| 字符串拼接 | 0.159s | 0.173s | 0.190s |
这里有两件不同的事都成立,值得分开来看。
.format() 稳定地最慢,差距大到噪声也掩盖不了。这一点背后有实实在在的原因:每次这一行运行,它都要查找方法、在运行时解析模板;而 f-string 已经编译成了直接拼出字符串的指令。
f-string 和 % 基本打平。在那七次运行里,f-string 四次最快,% 三次最快。几乎所有人都只跑一次脚本,然后对一个随运行时机而翻转的排名深信不疑。
不过还是看看量级。那是一百万次操作,所以即使是和 .format() 之间实打实的差距,每个字符串也只有大约 0.06 微秒。想省下一毫秒,你得格式化大约一万五千个字符串。
所以:f-string 很快,但速度几乎从来不是用它的理由。用它,是因为值就写在读它的地方。速度只是个不用操心的附赠。
速度真正要紧的地方有两个,都很好认:紧凑循环里的格式化,以及被抑制的日志。后者的问题其实不在速度,而在白做的工作。其他地方的瓶颈都在数据库、网络、JSON 解析,或者某个做了多余工作的算法。如果你觉得自己代码里的格式化很慢,就去对那段代码做性能分析,别凭直觉去重写 f-string。
简短版
如果只记五件事:
- f-string 是编译出来的,不是存储起来的。没有模板,所以它没法复用,也因此很快。
- 冒号后面是另一门更老的语言,词序固定。
,.2f是其中最有用的五个字符。 - 调试用
f"{x=}";凡是报告某个值出错的地方,用!r。 - 规范会交给
type(value).__format__。所以datetime用%Y,你自己的类两行代码就能加入进来。 - SQL 里的值用参数传,永远不要插值。咬你一口的,永远是那个你没料到的引号。
剩下的是宽度和对齐,用到时查一下就行。