Python 四种字符串格式化方式
格式化(formatting)是指把数据填写到预先定义的文本模板里面,形成一条用户可读的消息,并把这条消息保存成字符串的过程。
% 格式化
Python 里面最常用的字符串格式化方式是采用 % 格式化操作符。
这个操作符左边的文本模板叫作格式字符串(format string),我们可以在操作符右边写上某个值或者由多个值所构成的元组(tuple),用来替换格式字符串里的相关符号。
例如,下面这段代码通过 % 操作符把难以阅读的二进制和十六进制数值,显示成十进制的形式。
python
a = 0b10111011
b = 0xc5f
print('Binary is %d, hex is %d' % (a, b))
# >>>
# Binary is 187, hex is 3167
格式字符串里面可以出现 %d 这样的格式说明符,这些说明符的意思是,% 右边的对应数值会以这样的格式来替换这一部分内容。格式说明符的写法来自 C 语言的 printf 函数,所以,常见的 printf 选项都可以当成 Python 的格式说明符来用,例如 %s、%x、%f 等,此外还可以控制小数点的位值,并指定填充与对齐方式。
但是,C 风格的格式字符串,在 Python 里有四个缺点。
第一个缺点是,如果 % 右侧那个元组里面的值在类型或顺序上有变化,那么程序可能会因为转换类型时发生不兼容问题而出现错误。例如一个简单的例子:
python
key = 'my_var'
value = 1.234
formatted = '%-10s = %.2f' % (key, value)
print(formatted)
# >>>
# my_var = 1.23
但如果把 key 跟 value 互换位置,那么程序就会在运行时出现异常。
python
reordered_tuple = '%-10s = %.2f' % (value, key)
# >>>
# TypeError: must be real number, not str
如果 % 右侧的写法不变,但左侧那个格式字符串里面的两个说明符对调了顺序,那么程序同样会发生这个错误。
python
reordered_string = '%.2f = %-10s' % (key, value)
# >>>
# TypeError: must be real number, not str
要想避免这种问题,必须经常检查 % 操作符左右两侧的写法是否相互兼容。
第二个缺点是,在填充模板之前,经常要先对准备填写进去的这个值稍微做一些处理,但这样一来,整个表达式可能就会写得很长,让人觉得比较混乱。
下面这段代码用来罗列厨房里的各种食材,现在的这种写法并没有对填入格式字符串里面的那三个值(也就是食材的编号 i、食材的名称 item,以及食材的数量 count)预先做出调整。
python
pantry = [
('avocados', 1.25),
('bananas', 2.5),
('cherries', 15),
]
for i, (item, count) in enumerate(pantry):
print('#%d: %-10s = %.2f' % (i, item, count))
# >>>
# #0: avocados = 1.25
# #1: bananas = 2.50
# #2: cherries = 15.00
如果想让打印出来的信息更好懂,那可能得把这几个值稍微调整一下,但是调整之后,% 操作符右侧的那个三元组就特别长,所以需要多行拆分才能写得下,这会影响程序的可读性。
python
for i, (item, count) in enumerate(pantry):
print('#%d: %-10s = %d' % (
i + 1,
item.title(),
round(count)))
# >>>
# #1: Avocados = 1
# #2: Bananas = 2
# #3: Cherries = 15
第三个缺点是,如果想用同一个值来填充格式字符串里的多个位置,那么必须在 % 操作符右侧的元组中相应地多次重复该值。
python
template = '%s loves food. See %s cook.'
name = 'Max'
formatted = template % (name, name)
print(formatted)
# >>>
# Max loves food. See Max cook.
如果想在填充之前把这个值修改一下,那么必须同时修改多处才行,例如,如果这次要填的不是 name 而是 name.title(),那就必须提醒自己,要把所有的 name 都改成 name.title()。若是有的地方改了,有的地方没改,那输出的信息可能就不一致了。
python
template = '%s loves food. See %s cook.'
name = 'brad'
formatted = template % (name.title(), name.title())
print(formatted)
# >>>
# Brad loves food. See Brad cook.
为了解决上面提到的一些问题,Python 的 % 操作符允许我们用 dict 取代 tuple,这样的话,我们就可以让格式字符串里面的说明符与 dict 里面的键以相应的名称对应起来,例如 %(key)s 这个说明符,意思就是用字符串(s)来表示 dict 里面名为 key 的那个键所保存的值。下面通过这种办法解决刚才讲的第一个缺点,也就是 % 操作符两侧的顺序不匹配问题。
python
key = 'my_var'
value = 1.234
old_way = '%-10s = %.2f' % (key, value)
new_way = '%(key)-10s = %(value).2f' % {'key': key, 'value': value} # Original
reordered = '%(key)-10s = %(value).2f' % {'value': value, 'key': key} # Swapped
assert old_way == new_way == reordered
这种写法还可以解决刚才讲的第三个缺点,也就是用同一个值替换多个格式说明符的问题。改用这种写法之后,我们就不用在 % 操作符右侧重复这个值了。
python
name = 'Max'
template = '%s loves food. See %s cook.'
before = template % (name, name) # Tuple
template = '%(name)s loves food. See %(name)s cook.'
after = template % {'name': name} # Dictionary
assert before == after
但是,这种写法会让刚才讲的第二个缺点变得更加严重,因为字典格式字符串的引入,我们必须给每一个值都定义键名,而且要在键名的右侧加冒号,格式化表达式变得更加冗长,看起来也更加混乱。把不采用 dict 的写法与采用 dict 的写法对比一下,可以更明确地意识到这种写法的缺点。
python
for i, (item, count) in enumerate(pantry):
before = '#%d: %-10s = %d' % (i + 1, item.title(), round(count))
after = '#%(loop)d: %(item)-10s = %(count)d' % {'loop': i + 1, 'item': item.title(), 'count': round(count)}
assert before == after
第四个缺点是,把 dict 写到格式化表达式里面会让代码变多。每个键都至少要写两次:一次是在格式说明符中,还有一次是在字典中作为键,另外,定义字典的时候,可能还要专门用一个变量来表示这个键所对应的值,而且这个变量的名称或许也和键名相同,这样算下来就是三次了。
python
soup = 'lentil'
formatted = 'Today\'s soup is %(soup)s.' % {'soup': soup}
print(formatted)
# >>>
# Today's soup is 1enti1.
除了要反复写键名,在格式化表达式里面使用 dict 的办法还会让表达式变得特别长,通常必须拆分为多行来写,同时,为了与格式字符串的多行写法相对应,定义字典的时候,也要一行一行地给每个键设定对应的值。
内置的 format 函数与 str 类的 format 方法
Python3 添加了高级字符串格式化(advanced string formatting)机制,它的表达能力比老式 C 风格的格式字符串要强,且不再使用 % 操作符。
下面这段代码,演示了这种新的格式化方式。在传给 format 函数的格式里面,逗号表示显示千位分隔符,^ 表示居中对齐。
python
a = 1234.5678
formatted = format(a, ',.2f')
print(formatted)
b = 'my string'
formatted = format(b, '^20s')
print('*', formatted, '*')
# >>>
# 1,234.57
# * my string *
如果 str 类型的字符串里面有许多值都需要调整格式,则可以调用 str 的新 format 方法。该方法不使用 %d 这样的 C 风格格式说明符。而是把格式有待调整的那些位置在字符串里面先用 {} 代替,然后按从左到右的顺序,把需要填写到那些位置的值传给 format 方法,使这些值依次出现在字符串中的相应位置。
python
key ='my_var'
value = 1.234
formatted = '{} = {}'.format(key, value)
print(formatted)
# >>>
# my_var = 1.234
可以在 {} 里写个冒号,然后把格式说明符写在冒号的右边,用以规定 format 方法所接收的这个值应该按照怎样的格式来调整。
python
formatted = '{:<10} = {:.2f}'.format(key, value)
print(formatted)
# >>>
# my_var = 1.23
这种写法的效果可以这样理解:系统先把 str.format 方法接收到的每个值传给内置的 format 函数,并找到这个值在字符串里对应的 {},同时将 {} 里面写的格式也传给 format 函数,例如系统在处理 value 的时候,传的就是 format(value,'.2f')。然后,系统会把 format 函数所返回的结果写在整个格式化字符串 {} 所在的位置。另外,每个类都可以通过 __format__
这个特殊的方法定制相应的逻辑,这样的话,format 函数在把该类实例转换成字符串时,就会按照这种逻辑来转换。
C 风格的格式字符串采用 % 操作符来引导格式说明符,所以如果要将这个符号照原样输出,那就必须转义,也就是连写两个 %。同理,在调用 str.format 的时候,如果想把 str 里面的 {、} 照原样输出,那么也得转义。
python
print('%.2f%%' % 12.5)
print('{} replaces {{}}'.format(1.23))
# >>>
# 12.50%
# 1.23 replaces {}
调用 str.format 方法的时候,也可以给 str 的 {} 里面写上数字,用来指代 format 方法在这个位置所接收到的参数值位置索引。以后即使这些 {} 在格式字符串中的次序有所变动,也不用调换传给 format 方法的那些参数。于是,这就避免了前面讲的第一个缺点所提到的那个顺序问题。
python
formatted = '{1} = {0}'.format(key, value)
print(formatted)
# >>>
# 1.234 = my_var
同一个位置索引可以出现在 str 的多个 {} 里面,这些{}指代的都是 format 方法在对应位置所收到的值。这就不需要把这个值重复地传给 format 方法,于是就解决了前面提到的第三个缺点。
python
name = 'Max'
formatted = '{0} loves food. See {0} cook.'.format(name)
print(formatted)
# >>>
# Max loves food. See Max cook.
然而,这个新的 str.format 方法并没有解决上面讲的第二个缺点。如果在对值做填充之前要先对这个值做出调整,那么用这种方法写出来的代码还是跟原来一样乱,阅读性差。
当然,这种 {} 形式的说明符,还支持一些比较高级的用法,例如可以查询 dict 中某个键的值,可以访问 list 里某个位置的元素,还可以把值转化成 Unicode 或 repr 字符串。下面这段代码把这三项特性结合了起来。
python
menu = {
'soup': 'lentil',
'oyster': 'kumamoto',
'special': 'schnitzel',
}
formatted = 'First letter is {menu[oyster][0]!r}'.format(menu=menu)
print(formatted)
# >>>
# First letter is 'k'
但是这些特性,依然不能解决前面提到的第四个缺点,也就是键名需要多次重复的那个问题。所以并不推荐大家用 str.format 方法。当然,还是必须掌握新的格式说明符所使用的这套迷你语言(mini language),可以在 str 的 {} 里面按照这套迷你语言的规则来指定冒号右侧的格式。系统内置的 format 函数也会用到这套规则。
插值格式字符串
Python 3.6 添加了一种新的特性,叫作插值格式字符串(interpolated format string,简称 f-string),可以解决上面提到的所有问题。新语法特性要求在格式字符串的前面加字母 f 作为前缀,这跟字母 b 与字母 r 的用法类似。
f-string 把格式字符串的表达能力发挥到了极致,它彻底解决了上文提到的第四个缺点,也就是键名重复导致的程序冗余问题。可以直接在 f-string 的 {} 里面引用当前 Python 范围内的所有名称,进而达到简化的目的。
python
key = 'my_var'
value = 1.234
formatted = f'{key} = {value}'
print(formatted)
# >>>
# my_var = 1.234
str.format 方法所支持的那套迷你语言,也就是在 {} 内的冒号右侧所采用的那套规则,现在也可以用到 f-string 里面,而且还可以像早前使用 str.format 时那样,通过 ! 符号把值转化成 Unicode 及 repr 形式的字符串。
python
formatted = f'{key!r:<10} = {value:.2f}'
print(formatted)
# >>>
# 'my_var' = 1.23
同一个问题,使用 f-string 来解决总是比通过 % 操作符使用 C 风格的格式字符串简单,而且也比 str.format 方法简单。
python
f_string = f'{key:<10} = {value:.2f}'
c_tuple = '%-10s = %.2f' % (key, value)
str_args = '{:<10} = {:.2f}'.format(key, value)
str_kw = '{key:<10} = {value:.2f}'.format(key=key, value=value)
c_dict = '%(key)-10s = %(value).2f' % {'key': key, 'value': value}
assert c_tuple == c_dict == f_string
assert str_args == str_kw == f_string
在 f-string 方法中,各种 Python 表达式都可以出现在 {} 里,于是这就解决了前面提到的第二个缺点。我们现在可以用相当简洁的写法对需要填充到字符串里面的值做出微调。
python
pantry = [
('avocados', 1.25),
('bananas', 2.5),
('cherries', 15),
]
for i, (item, count) in enumerate(pantry):
old_style = '#%d: %-10s = %d'% (
i + 1,
item.title(),
round(count))
new_style = '#{}: {:<10s} = {}'.format(
i + 1,
item.title(),
round(count))
f_string = f'#{i+1}: {item.title():<10s} = {round(count)}'
assert old_style == new_style == f_string
要是想表达得更清楚一些,可以把 f-string 写成多行的形式,类似于 C 语言的相邻字符串拼接(adjacent-string concatenation)。
python
pantry = [
('avocados', 1.25),
('bananas', 2.5),
('cherries', 15),
]
for i, (item, count) in enumerate(pantry):
print(f'#{i+1}: '
f'{item.title():<10s} = '
f'{round(count)}')
# >>>
# #1: Avocados = 1
# #2: Bananas = 2
# #3: Cherries = 15
Python 表达式也可以出现在格式说明符中。例如,下面的代码把小数点之后的位数用变量来表示,然后把这个变量的名字 places 用 {} 括起来放到格式说明符中,这样写比采用硬代码更灵活。
python
places = 3
number = 1.23456
print(f'My number is {number:.{places}f}')
# >>>
# My number is 1.235
在 Python 内置的四种字符串格式化办法里面,f-string 可以简洁而清晰地表达出许多种逻辑,这使它成为程序员的最佳选择。如果你想把值以适当的格式填充到字符串里面,那么首先应该考虑的就是采用 f-string 来实现。