Python3.6 的 f-strings
更新:
- 2024-04-19,重写全文,根据使用场景分类,方便作为 cheatsheet
引言
字符串格式化绝对可以算得上日常生活中最为常用的功能之一,我们经常需要输出各种字符串还需要精确控制其格式
在一些过时的 Python 教程上,还可以看到使用 %
来格式化字符串。但其实在 Python3.6 之后,f-strings 已经成了格式化字符串的最优解,优点包括:
- 可以在字符串字面值(String literal)里面内嵌表达式
- 可读性非常强
下面的简单对比就可以看出 f-strings 在可读性上的优势:
name = "Martin"
f"My name is {name}"
'My name is Martin'
name = "Martin"
"My name is %s" % name
'My name is Martin'
在上面的例子中我们分别用两种格式化字符串的方式实现了同样功能
看一眼 f-strings 就知道这个字符串是要输出名字(当然这要求你用的是有意义的变量,比如这里用的是 name
)。而如果用旧的 %
字符串格式化字符串,则稍微显得不那么自然,我们首先会看到 %s
占位符,此时我们需要向右看才知道这里会放置什么变量
这里的例子不长所以没啥差别,但是如果用 %
格式化的字符串很长的话,看代码就会有割裂感,你的眼睛需要来回左右移动。而且这不仅仅是阅读代码上不大自然,如果是自己写很长的字符串,还得核对一遍占位符和变量的顺序是否一致,这无疑是一个负担 😫
语法规则
f-strings 的语法规则很简单,简单来说,它只是在普通字符串("..."
or '...'
) 的前面加上了 f
前缀,这样你就得到了一个 f-strings
f"..."
# or f'...'
在 f-strings 里面,我们可以放置形如 {expression}
的文本,它的意思是:Python 会帮我们对这个表达式求值,并进行格式化的输出
当然,只是求值还不够,有时候我们想要进行各种控制
- 浮点数的精度
- 是否要显示正负数的
+
或者是-
- …
而这些则是通过 format_specifier
进行控制,它包含了多个可选的配置项。从语法上看,format_specifier
接在 expression
后面,两者之间用 :
隔开,所以确切来说,f-strings 的语法规则是下面这样的
f"...{expression:format_specifier}..."
下面我们来按照功能进行分类,看看用 f-strings 我们都可以做些什么
各种使用场景
基本使用
我们先考虑最简单的情况:不包含任何 format_specifier
。下面是一个简单的例子
left, right = 3, 5
f"{left} + {right} = {left + right}"
'3 + 5 = 8'
{}
来充当占位符就很自然引申出一个问题——如果要在字符串里面输出 {}
这两个字符怎么办?答案也很简单,我们只要用 {{}}
即可f"{{}}"
'{}'
值得一提的还有,expression
里面不允许出现 :
和 !
和 \
实际上 expression
里面也很少会包含这几个字符。\
的一个使用场景是用来转义 '
或者 "
,但其实只要我们内外用的是不同的引号格式即可
至于 :
,在 PEP4981 里面举例可能应用场景是 lambda 表达式,此时我们只需用一对括号将 lambda 表达式括起来即可:
# Note that we need to add () around the lambda expression
f"{(lambda x: x + 1)(3)}"
'4'
不同表示
Python 对象一般会有 2 种字符串的表示,分别对应 __str__
和 __repr__
方法,其中前者一般是面向客户的,可读性较好;后者一般是面向开发者的,便于调试程序。f-strings 对此提供了支持,我们可以指定要用哪一种
{foo:!s} # it's equal to call str(foo) first
{foo:!r} # similarily, call repr(foo) first
{foo:!a} # similarily,call ascii(foo) first
补齐与对齐
补齐和对齐涉及到下面几个问题
- 要用什么字符补齐?下面用
fill
表示 - 要补齐到多长?下面用
width
表示 - 要如何对齐?下面用
align
表示
因此,补齐和对齐的语法规则是 [[fill]align][width]
[]
表示这是可选的部分其中 align
支持下面这几种对齐方式:
< # left-aligned
> # right-aligned
^ # centered
= # only valid for numeric types, pad between sign and digits
举例如下
f"{-1:*^9}" # set `fill` to *, and set width to 9
'***-1****'
f"{-1:*>9}" # set `fill` to *, and set width to 9
'*******-1'
f"{-1:*<9}" # set `fill` to *, and set width to 9
'-1*******'
f"{-1:*=9}" # set `fill` to *, and set width to 9
'-*******1'
符号位的显示
支持下面这几种
+ # both positive and negative
- # only negative (default)
space # a leading spaces for positive and minus sign for negative
f"{1:+}"
'+1'
f"{-1:+}"
'-1'
assert f"{1}" == f"{1:-}" # because it's the default behavior
浮点数精度
浮点数要保留 n
位小数我们就用 .nf
,跟 C/C++ 的 printf
有点像,比如
x = 1.23456
f"{x:.1f}"
'1.2'
x = 1.23456
f"{x:.3f}"
'1.235'
-0.0 的问题
从 Python3.11 起,增加了可选的 z
用来处理 -0.
。因为调查显示2,大多数情况下我们都不想得到 -0.0
这样的输出
x = -0.0001
f"{x:.1f}" # set the precision to 1, so it will round to -0.0
'-0.0'
x = -0.0001
f"{x:z.1f}" # with z, we will get 0.0 rather than -0.0
'0.0'
不同进制的表示
Python 提供了几个方便的函数,比如 bin
,oct
等来获得不同进制的表示,根据 f-strings 的定义,我们可以选择写出 {bin(a)}
来获得 a
的二进制的表示(其他进制的表示同理)。但 f-strings 其实已经帮你考虑到这点了,它默认支持如下的进制表示
b # base 2
o # base 8
d # base 10
x # base 16, low-case letters
X # base 16, upper-case letters
#
,就会添加对应进制表示的前缀,比如 0b
等f"{15:b}" # represent 15 in base 2
'1111'
f"{15:#b}" # represent 15 in base 2, use # to add prefix 0b
'0b1111'
f"{15:#X}" # represent 15 in base 16, use # to add prefix 0X
'0XF'
千分位分隔符
在金融或者经济学里面,经常会用 ,
作为千分位分隔符,f-strings 对此提供了支持,除此之外,还可以指定 _
作为千分位的分隔符34:
f"{123456789:,}"
'123,456,789'
f"{1234.56789:,}"
'1,234.56789'
f"{123456789:_}"
'123_456_789'
f"{1234.56789:_}"
'1_234.56789'
Trick
从 Python3.8 起,我们可以用 {expression=}
的形式,它会自动帮我们转换成 expression={expression}
5,这有什么用呢?
在调试 Python 程序的时候,我经常会写出如下的 f-strings 输出我关心的变量的值,但有点麻烦的是,我不得不写两次变量的名字
f"left={left}, right={right}"
'left=3, right=5'
可以看到,里面存在一些冗余,因此,一个更好的方式是
f"{left=}, {right=}"
'left=3, right=5'
总结
在我刚开始学习 Python 的时候,当时的教程都是在用 %
来格式化输出,后来的推荐是 str.format
方法。而到了 f-string 随着 Python3.6 发布之后,似乎统一了字符串输出的 Best practice,大家都在用这个。这也是符合 Python 设计哲学的——应该只有一种显而易见的方式做到一件事🚀