第4条:使用F字符串替代C风格字符串和str.format
Item 4: Prefer Interpolated F-String Over C-style Format Strings and str.format
字符串贯穿Python的始终。可以用来在用户界面呈现信息和命令行工具。可以用来写入数据到文件和sockets。可以用来描述异常。用来debug。 格式化(Formatting) 字符串是将预先定义的文本和数据值结合成可读的信息,存储在字符串中。Python有4种格式化字符串方法(C风格字符串,模板,str.format和f-字符串。也可以将模板方法当成是C风格字符串的改进)。
这些方法中推荐f-字符串,简单易用,可以看成是对前面几种方法的改进。如果对其它方法不感兴趣,可以直接跳到4. f-字符串
1. C风格字符串
(在以前)最常见的是通过%进行格式化(类似于C的格式化字符串)。
a = 0b10111011
b = 0xc5f
print('Binary is %d, hex is %d' % (a, b))
output:
Binary is 187, hex is 3167
这种方式使用格式说明符(如%d)
作为占位符(placeholders),占位符将被 %
右侧的实际值替代。这种方式来自C语言的printf函数。Python支持各种常见的说明符,如%s,%x,%f
等。Python中C-style的格式化字符串有如下4个问题:第一个问题就是如果你改变data values的类型或顺序时,格式说明符也要相应修改,否则就会出错。(顺序敏感)
key = 'my_var'
value = 1.234
#correct
formatted = '%-10s = %.2f' % (key,value)
print(formatted)
#改变数据顺序,就会出错
formatted = '%-10s = %.2f' % (value,key)
print(formatted)
output:
Binary is 187, hex is 3167
my_var = 1.23
Traceback (most recent call last):
File "D:/PY_TEST/python90/4_format_string.py", line 10, in
formatted = '%-10s = %.2f' % (value,key)
TypeError: must be real number, not str
第二个问题是C-style格式字符串可读性很差。尤其是当你想做一些修改的时候。
# second problem
pantry = [
('avocados', 1.25),
('bananas', 2.5),
('cherries', 15),
]
for i , (item, count) in enumerate(pantry):
print('#%d: %-10s = %.2f' % (i, item, count))
# make a few modifications to values,become so long
for i , (item, count) in enumerate(pantry):
print('#%d: %-10s = %d' % (
i + 1,
item.title(),
round(count)))
第三个问题是如果你想用同一个值多次,你需要在%右侧重复多次。
template = '%s loves food. See %s cook.'
name = 'Max'
formatted = template % (name, name)
print(formatted)
重复容易导致错误,当你做一些修改的时候,很容易忘记某处,导致不一致。
2. 字典格式化字符串(模板)
为了解决其中的一些问题,Python中%右侧可以使用字典(dictionary)而不是元组(tuple)。这可以解决上面的问题1(顺序敏感)。
# dict
key = 'my_var'
value = 1.234
old_way = '%-10s = %.2f' % (key, value)
new_way = '%(key)-10s = %(value).2f' % {
'key': key, 'value': value}
reordered = '%(key)-10s = %(value).2f' % {
'value': value, 'key': key}
assert old_way == new_way == reordered
字典也解决了问题3(重复).
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
但是,字典格式化字符串引入了一些其它问题。例如问题二,使用字典会使语句更长。
# make other problems
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
使用字典格式化字符串同样增加冗长。也就是问题4。每个key至少出现两次,一次在描述符,一次在字典中,并且还可能作为变量名出现。
# verbosity, soup 出现3次。
soup = 'lentil'
formatted = 'Today\'s soup is %(soup)s.' % {'soup': soup}
print(formatted)
menu = {
'soup': 'lentil',
'oyster': 'kumamoto',
'special': 'schnitzel',
}
template = ('Today\'s soup is %(soup)s, '
'buy one get two %(oyster)s oysters, '
'and our special entrée is %(special)s.')
formatted = template % menu
print(formatted)
为了理解格式化字符串会产生什么,需要一边看menu,一边看template,可读性变差。需要有更好的方式来格式化字符串。
3. 内置的str.format
# format
a = 1234.5678
formatted = format(a, ',.2f')
print(formatted)
b = 'my string'
formatted = format(b,'^20s')
print(formatted)
可以简单使用{}作为占位符,不用%d这种。
key = 'my_var'
value = 1.234
formatted = '{} = {}'.format(key, value)
print(formatted)
也可以在{}内使用:说明符
key = 'my_var'
value = 1.234
formatted = '{:<10} = {:.2f}'.format(key, value)
print(formatted)
格式化操作可以在每个类的__format__
方法里修改。在C字符串里,如果想输出%
,应该用%%
,同样,str.format里输出{}
要用{{}}
print('%.2f%%' % 12.5)
print('{} replaces {{}}'.format(1.23))
{}内也可以指定索引,这允许字符串更新顺序,而不需要改变右侧的内容。解决了问题1(顺序敏感).
formatted = '{1} = {0}'.format(key, value)
也可以解决问题3,重复的问题。
formatted = '{0} loves food. See {0} cook.'.format(name)
可惜的是,format方法没有解决问题2。看起来依旧难读。
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))
assert old_style == new_style
使用format有更好的选项:
formatted = 'First letter is {menu[oyster][0]!r}'.format(
menu=menu)
print(formatted)
但是问题4,重复key导致冗余的问题还是存在。
old_template = (
'Today\'s soup is %(soup)s, '
'buy one get two %(oyster)s oysters, '
'and our special entrée is %(special)s.')
old_formatted = template % {
'soup': 'lentil',
'oyster': 'kumamoto',
'special': 'schnitzel',
}
new_template = (
'Today\'s soup is {soup}, '
'buy one get two {oyster} oysters, '
'and our special entrée is {special}.')
new_formatted = new_template.format(
soup='lentil',
oyster='kumamoto',
special='schnitzel',
)
assert old_formatted == new_formatted
看起来好了一些,因为减少了字典中的一些引号。但还是很冗长。
鉴于这些问题的存在,总体上不推荐str.format。
4. f-字符串
Python3.6 添加了插值格式字符串(Interpolated Format Strings),简称为f-stirngs,来解决上述问题。使用时在字符串前加上f
,就像byte string加前缀b
,原始字符串加前缀r
。
key = 'my_var'
value = 1.234
formatted = f'{key} = {value}' # f-字符串
print(formatted)
str.format的操作都可以在f字符串中使用。
formatted = f'{key!r:<10} = {value:.2f}'
print(formatted)
f字符串比之前的更简洁
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字符串还允许将Python表达式放入{}内,解决了问题2(可读性差)。
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字符串拆成多行,看起来会更加清晰。
for i, (item, count) in enumerate(pantry):
print(f'#{i+1}: '
f'{item.title():<10s} = '
f'{round(count)}')
Python表达式也可以出现在格式说明符中:
places = 3
number = 1.23456
print(f'My number is {number:.{places}f}')
F字符串的强大特性使其成为使用格式化字符串时的首选。
Things to Remember
• C-style格式化字符串使用%,有一些缺陷和冗余。
• str.format引入很多有用的概念,但也重复了C-style字符串的问题。
• F-strings 是一种新的格式化字符串方法,解决了C字符串的最大问题。
• F-strings 简洁强大,允许直接内嵌Python表达式。