
第4条 用支持插值的f-string取代C风格的格式字符串与str.format方法
Python代码里面有很多地方都会出现字符串。在用户界面与命令行工具中显示消息的时候要用,把数据写到文件与socket的时候要用,在Exception
里面详细描述出错情况时也要用(参见第27条),还有就是调试程序的时候同样需要使用字符串(参见第80条与第75条)。
格式化(formatting)是指把数据填写到预先定义的文本模板里面,形成一条用户可读的消息,并把这条消息保存成字符串的过程。用Python对字符串做格式化处理有四种办法可以考虑,这些办法都内置在语言和标准库里面。但其中三种办法有严重的缺陷,笔者先解释为什么不要使用这三种办法,最后再给出剩下的那一种。
Python里面最常用的字符串格式化方式是采用%格式化操作符。这个操作符左边的文本模板叫作格式字符串(format string),我们可以在操作符右边写上某个值或者由多个值所构成的元组(tuple),用来替换格式字符串里的相关符号。例如,下面这段代码通过%
操作符把难以阅读的二进制和十六进制数值,显示成十进制的形式。

格式字符串里面可以出现%d
这样的格式说明符,这些说明符的意思是,%
右边的对应数值会以这样的格式来替换这一部分内容。格式说明符的写法来自C语言的printf
函数,Python语言以及其他一些编程语言,都依照那套写法来规定自己的格式字符串。所以,常见的printf
选项都可以当成Python的格式说明符来用,例如%s
、%x
、%f
等,此外还可以控制小数点的位值,并指定填充与对齐方式。许多Python新手程序员都喜欢用C风格的格式字符串,因为他们比较熟悉这种风格,而且这样写起来比较简单。
C风格的格式字符串,在Python里有四个缺点。
第一个缺点是,如果%
右侧那个元组里面的值在类型或顺序上有变化,那么程序可能会因为转换类型时发生不兼容问题而出现错误。例如,下面这个简单的格式化表达式是正确的。

但如果把key
跟value
互换位置,那么程序就会在运行时出现异常。

如果%
右侧的写法不变,但左侧那个格式字符串里面的两个说明符对调了顺序,那么程序同样会发生这个错误。

要想避免这种问题,必须经常检查%
操作符左右两侧的写法是否相互兼容。这个过程很容易出错,因为每次修改完之后都要手工检查一遍。
第二个缺点是,在填充模板之前,经常要先对准备填写进去的这个值稍微做一些处理,但这样一来,整个表达式可能就会写得很长,让人觉得比较混乱。下面这段代码用来罗列厨房里的各种食材,现在的这种写法并没有对填入格式字符串里面的那三个值(也就是食材的编号i
、食材的名称item
,以及食材的数量count
)预先做出调整。

如果想让打印出来的信息更好懂,那可能得把这几个值稍微调整一下,但是调整之后,%
操作符右侧的那个三元组就特别长,所以需要多行拆分才能写得下,这会影响程序的可读性。

第三个缺点是,如果想用同一个值来填充格式字符串里的多个位置,那么必须在%
操作符右侧的元组中相应地多次重复该值。

如果想在填充之前把这个值修改一下,那么必须同时修改多处才行,这尤其烦人,且容易出错。例如,如果这次要填的不是name
而是name.title()
,那就必须提醒自己,要把所有的name
都改成name.title()
。若是有的地方改了,有的地方没改,那输出的信息可能就不一致了。

为了解决上面提到的一些问题,Python的%
操作符允许我们用dict
取代tuple
,这样的话,我们就可以让格式字符串里面的说明符与dict
里面的键以相应的名称对应起来,例如%(key)s
这个说明符,意思就是用字符串(s
)来表示dict
里面名为key
的那个键所保存的值。下面通过这种办法解决刚才讲的第一个缺点,也就是%
操作符两侧的顺序不匹配问题。

这种写法还可以解决刚才讲的第三个缺点,也就是用同一个值替换多个格式说明符的问题。改用这种写法之后,我们就不用在%
操作符右侧重复这个值了。

但是,这种写法会让刚才讲的第二个缺点变得更加严重,因为字典格式字符串的引入,我们必须给每一个值都定义键名,而且要在键名的右侧加冒号,格式化表达式变得更加冗长,看起来也更加混乱。我们把不采用dict
的写法与采用dict
的写法对比一下,可以更明确地意识到这种写法的缺点。

在Python中应用C风格格式化表达式的第四个缺点是,把dict
写到格式化表达式里面会让代码变多。每个键都至少要写两次:一次是在格式说明符中,还有一次是在字典中作为键,另外,定义字典的时候,可能还要专门用一个变量来表示这个键所对应的值,而且这个变量的名称或许也和键名相同,这样算下来就是三次了。

除了要反复写键名,在格式化表达式里面使用dict
的办法还会让表达式变得特别长,通常必须拆分为多行来写,同时,为了与格式字符串的多行写法相对应,定义字典的时候,也要一行一行地给每个键设定对应的值。

为了查看格式字符串中的说明符究竟对应于字典里的哪个键,必须在这两段代码之间来回跳跃,这会令人难以发现其中的bug。如果要对键名稍做修改,那么必须同步修改格式字符串里的说明符,这更让代码变得相当烦琐,可读性更差。
肯定有更好的办法才对。
内置的format函数与str类的format方法
Python 3添加了高级字符串格式化(advanced string formatting)机制,它的表达能力比老式C风格的格式字符串要强,且不再使用%
操作符。我们针对需要调整格式的这个Python值,调用内置的format
函数,并把这个值所应具备的格式也传给该函数,即可实现格式化。下面这段代码,演示了这种新的格式化方式。在传给format
函数的格式里面,逗号表示显示千位分隔符,^表示居中对齐。

如果str
类型的字符串里面有许多值都需要调整格式,则可以调用str
的新format
方法。该方法不使用%d
这样的C风格格式说明符。而是把格式有待调整的那些位置在字符串里面先用{}
代替,然后按从左到右的顺序,把需要填写到那些位置的值传给format
方法,使这些值依次出现在字符串中的相应位置。

你可以在{}
里写个冒号,然后把格式说明符写在冒号的右边,用以规定format
方法所接收的这个值应该按照怎样的格式来调整。在Python解释器里输入help('FORMATTING')
,可以详细查看str.format
使用的这套格式说明符所依据的规则。

这种写法的效果可以这样理解:系统先把str.format
方法接收到的每个值传给内置的format
函数,并找到这个值在字符串里对应的{}
,同时将{}
里面写的格式也传给format
函数,例如系统在处理value
的时候,传的就是format(value, '.2f')
。然后,系统会把format
函数所返回的结果写在整个格式化字符串{}
所在的位置。另外,每个类都可以通过__format__
这个特殊的方法定制相应的逻辑,这样的话,format
函数在把该类实例转换成字符串时,就会按照这种逻辑来转换。
C风格的格式字符串采用%
操作符来引导格式说明符,所以如果要将这个符号照原样输出,那就必须转义,也就是连写两个%
。同理,在调用str.format
的时候,如果想把str
里面的{
、}
照原样输出,那么也得转义。

调用str.format
方法的时候,也可以给str
的{}
里面写上数字,用来指代format
方法在这个位置所接收到的参数值位置索引。以后即使这些{}
在格式字符串中的次序有所变动,也不用调换传给format
方法的那些参数。于是,这就避免了前面讲的第一个缺点所提到的那个顺序问题。

同一个位置索引可以出现在str
的多个{}
里面,这些{}
指代的都是format
方法在对应位置所收到的值。这就不需要把这个值重复地传给format
方法,于是就解决了前面提到的第三个缺点。

然而,这个新的str.format
方法并没有解决上面讲的第二个缺点。如果在对值做填充之前要先对这个值做出调整,那么用这种方法写出来的代码还是跟原来一样乱,阅读性差。把原来那种写法和现在的新写法对比一下,大家就会看到新写法并不比原来好多少。

当然,这种{}
形式的说明符,还支持一些比较高级的用法,例如可以查询dict
中某个键的值,可以访问list
里某个位置的元素,还可以把值转化成Unicode或repr
字符串。下面这段代码把这三项特性结合了起来。

但是这些特性,依然不能解决前面提到的第四个缺点,也就是键名需要多次重复的那个问题。下面把C风格的格式化表达式与新的str.format
方法对比一下,看看这两种写法在处理键值对形式的数据时有什么区别。

新写法稍微好一点儿,因为它不用定义dict
了,所以不需要把键名用' '给括起来。它的说明符也比旧写法的说明符要简单一些。然而这些优点并不突出。另外,虽然我们在新写法里面,可以访问字典中的键,也可以访问列表中的元素,但这些功能只涵盖了Python表达式的一小部分特性,str.format
方法还是没有能够把Python表达式的优势充分发挥出来。
因为str.format
方法有这样的一些缺点,而且没办法解决早前提到的第二个与第四个缺点,所以总体来说,笔者并不推荐大家用str.format
方法。当然,我们还是必须掌握新的格式说明符所使用的这套迷你语言(mini language),我们可以在str
的{}
里面按照这套迷你语言的规则来指定冒号右侧的格式。系统内置的format
函数也会用到这套规则。除此之外,str.format
方法就只剩下历史意义了,它让我们可以在这套机制的基础之上学习Python新引入的f-string。接下来,我们就来看看f-string为什么比前面几种办法都要强大。
插值格式字符串
Python 3.6添加了一种新的特性,叫作插值格式字符串(interpolated format string,简称f-string),可以解决上面提到的所有问题。新语法特性要求在格式字符串的前面加字母f
作为前缀,这跟字母b
与字母r
的用法类似,也就是分别表示字节形式的字符串与原始的(或者说未经转义的)字符串的前缀。
f-string把格式字符串的表达能力发挥到了极致,它彻底解决了上文提到的第四个缺点,也就是键名重复导致的程序冗余问题。我们不用再像使用C风格格式表达式时那样专门定义dict
,也不用再像调用str.format
方法时那样专门把值传给某个参数,这次可以直接在f-string的{}
里面引用当前Python范围内的所有名称,进而达到简化的目的。

str.format
方法所支持的那套迷你语言,也就是在{}
内的冒号右侧所采用的那套规则,现在也可以用到f-string里面,而且还可以像早前使用str.format
时那样,通过!符号把值转化成Unicode及repr
形式的字符串。

同一个问题,使用f-string来解决总是比通过%
操作符使用C风格的格式字符串简单,而且也比str.format
方法简单。下面按照从短到长的顺序把这几种写法所占的篇幅对比一下,每种写法里面的那个赋值符号(=)左侧都对齐到同一个位置,这样很容易看出符号右边的代码到底有多少。

在f-string方法中,各种Python表达式都可以出现在{}
里,于是这就解决了前面提到的第二个缺点。我们现在可以用相当简洁的写法对需要填充到字符串里面的值做出微调。C风格的写法与采用str.format
方法的写法可能会让表达式变得很长,但如果改用f-string,或许一行就能写完。

要是想表达得更清楚一些,可以把f-string写成多行的形式,类似于C语言的相邻字符串拼接(adjacent-string concatenation)。这样写虽然比单行的f-string要长,但仍然好过另外那两种多行的写法。

Python表达式也可以出现在格式说明符中。例如,下面的代码把小数点之后的位数用变量来表示,然后把这个变量的名字places
用{}
括起来放到格式说明符中,这样写比采用硬代码更灵活。

在Python内置的四种字符串格式化办法里面,f-string可以简洁而清晰地表达出许多种逻辑,这使它成为程序员的最佳选择。如果你想把值以适当的格式填充到字符串里面,那么首先应该考虑的就是采用f-string来实现。
要点
- 采用
%
操作符把值填充到C风格的格式字符串时会遇到许多问题,而且这种写法比较烦琐。 str.format
方法专门用一套迷你语言来定义它的格式说明符,这套语言给我们提供了一些有用的概念,但是在其他方面,这个方法还是存在与C风格的格式字符串一样的多种缺点,所以我们也应该避免使用它。- f-string采用新的写法,将值填充到字符串之中,解决了C风格的格式字符串所带来的最大问题。
- f-string是个简洁而强大的机制,可以直接在格式说明符里嵌入任意Python表达式。