Blog

Python f-string 完全指南

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 sra 三者之一
格式规范(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'"

并没有一个中央格式化引擎。intfloatstrDecimaldatetime 各自实现了自己的一套,所以它们都认得 ,.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 里的值用参数传,永远不要插值。咬你一口的,永远是那个你没料到的引号。

剩下的是宽度和对齐,用到时查一下就行。

这篇文章对你有帮助吗?

点一颗爱心来评分!

平均评分 0 / 5. 投票总数: 0

还没有人投票。来做第一个评分的人吧。