Python高效编程之88条军规(2):你真的会格式化字符串吗?

在微信公众号「极客起源」中输入595586,可学习所有的《Python高效编程之88条军规》系列文章。

在Python语言中,字符串有多种用途。 能够用于在用户界面和命令行实用程序中显示消息; 用于用于将数据写入文件和Socket; 用于指定“异常”消息; 用于调试程序。
格式化是将预约义的文本和数据组合成一条人类可读的消息的过程。Python具备4种不一样的格式化字符串的方式,这4种方式有的是语言层面支持的,有的是经过标准库支持的。除其中一种方式外,其余的格式化方式都有严重的缺点,在使用时应该尽可能避免这些缺陷。
1.  C风格的字符串格式化方式
在Python语言中格式化字符串的最多见方法是使用%格式化运算符。预约义的文本模板以格式字符串的形式放在%运算符的左侧,要插入模板的数据在%运算符的右侧。这些数据能够是单个值,也能够是一个元组(不能是列表),表示将多个值插入模板。例如,在这里我使用%运算符将难以阅读的二进制和十六进制值转换为整数字符串:
a = 0b10111010b = 0xc5cprint('二进制:%d, 十六进程:%d' % (a, b))
执行这段代码,会输出以下内容:
二进制:186, 十六进程:3164
格式字符串使用格式说明符(如%d)做为占位符,这些占位符将被%运算符右侧的值替换。格式说明符的语法来自C语言的printf函数,该函数已被Python(以及其余编程语言)继承。Python支持全部经常使用的printf函数格式化选项。例如%s,%x和%f格式说明符,以及对小数位,填充,填充和对齐的控制。许多不熟悉Python的程序员都以C风格的格式字符串开头,由于它们熟悉且易于使用。
可是使用C风格的格式化字符串方式,会带来以下4个问题:
问题1:
若是更改格式表达式右侧的元组中数据值的类型或顺序,可能会因为类型转换不兼容而抛出异常。例如,这个简单的格式表达式能够工做:
key = 'my_key'value = 1.234formatted = '%-10s = %.2f' % (key, value)print(formatted)
执行这段代码,会输出以下内容:
my_key = 1.23
但如何交换key和value的值,那将会抛出运行时异常:
key = 1.234value = 'my_key'formatted = '%-10s = %.2f' % (key, value)print(formatted)
执行这段代码,会抛出以下异常:
Traceback (most recent call last): File "/python/format.py", line 12, in <module> formatted = '%-10s = %.2f' % (key, value)TypeError: must be real number, not str
相似地,若是%右侧元组中值的顺序变化后,一样会抛出异常。
formatted = '%-10s = %.2f' % (key, value)
为了不这种麻烦,你须要不断检查%运算符的两侧的数据类型是否匹配;此过程容易出错,由于每次修改代码,都必须人工检测数据类型是否匹配。
问题2:
C风格格式化表达式的第2个问题是当你须要在将值格式化为字符串以前对值进行小的修改时,它们将变得难以阅读,这是很是广泛的需求。在这里,我列出了厨房储藏室的内容,而没有进行内联更改:
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
如今,我对要格式化的值进行了一些修改,以便打印出更有用的信息。这致使格式化表达式中的元组变得太长,以致于须要将其分红多行,这会损害程序的可读性:
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
问题3:
格式化表达式的第3个问题是若是要在格式字符串中屡次使用相同的值,则必须在右侧重复该值屡次:
template = '%s loves food. See %s cook.'name = 'Max'formatted = template % (name, name)print(formatted)
执行这段代码,会输出以下的内容:
Max loves food. See Max cook.
若是须要对这些重复的值作一些小的修改,这将特别使人讨厌的事,并且很是容易出错。为了解决这个问题,推荐使用字典取代元组为格式化字符串提供数据。引用字典中值的方式是%(key),看下面的例子:
old_way = '%-10s , %.2f, %-8s' % (key, value,key) # 重复指定key
new_way = '%(key)-10s , %(value).2f, %(key)-8s' % { 'key': key, 'value': value} # 只须要指定一次key
print(old_way)print(new_way)
执行这段代码,会输出以下的内容:
key1 , 1.13, key1 key1 , 1.13, key1
咱们能够看到,若是须要重复引用%右侧的值,在使用元组的状况下,须要重复指定这些值,如本例中的key。而使用字典,只须要指定一次key就能够了。
而后,使用字典格式化字符串会引入并加重其余问题。对于上面的问题2,因为在格式化以前对值进行了小的修改,因为%运算符右侧存在键和冒号运算符,所以格式化表达式变得更长,而且在视觉上更加杂乱。在下面的代码中,我分别使用字典和不使用指点来格式化相同的字符串以说明此问题:
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:
使用字典格式化字符串还会带了第4个问题,就是每一个键必须至少指定两次:在格式说明符中指定一次,另外一次是在字典中指定为键,若是字典值自己是一个变量,也须要再次指定。
soup = 'lentil'formatted = 'Today\'s soup is %(soup)s.' % {'soup': soup} # 这里再次指定了变量soupprint(formatted)
输出结果以下:
Today's soup is lentil.
除了重复字符以外,这种冗余还会致使使用字典的格式化表达式很长。这些表达式一般必须跨多行,格式字符串跨多行链接,而且字典赋值每一个值只有一行用于格式化:
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 % menuprint(formatted)
输出结果以下:
Today's soup is lentil, buy one get two kumamoto oysters, and our special entrée is schnitzel.
因为格式化字符串很长,可能会跨多行,因此要想了解整个字符串想表达什么,你的眼镜必须上下左右来回移动,并且很容易忽略本应该发现的错误。那么是否有更好的格式化字符串的解决方案呢?请继续往下看:
2. 内建format函数与str.format方法
Python 3添加了对高级字符串格式化的支持,这种格式化方式比使用%运算符的C风格格式化字符串更具表现力。对于单独的值,能够经过格式化内建函数来访问此新功能。例如,下面的代码使用一些新选项(,用于千分位分隔符,使用^用于居中)来格式化值:
a = 1234.5678formatted = format(a, ',.2f')print(formatted)
b = 'my string'formatted = format(b, '^20s') # 居中显示字符串print('*', formatted, '*')
运行结果以下:
1,234.57* my string *
您能够经过调用字符串的format方法来格式化多个值。format方法使用{}做为占位符,而不是使用%d这样的C风格格式说明符。在默认状况下,格式化字符串中的占位符按着它们出现的顺序传递给format方法相应位置的占位符。
key = 'my_var'value = 1.234
formatted = '{} = {}'.format(key, value)print(formatted)
运行结果以下:
my_var = 1.234
每一个占位符内能够在冒号(:)后面指定格式化说明符,用来指定将值转换为字符串的方式,代码以下:
formatted = '{:<10} = {:.2f}'.format(key, value)print(formatted)
运行结果以下:
my_var = 1.23
format方法的工做原理是将格式化说明符与值(上例中的format(value,'.2f'))一块儿传递给内建函数format。而后将 该函数的返回值替换对应的占位符。可使用__format__方法针对每一个类自定义格式化行为。
对于C风格的格式化字符串,须要对%运算符进行转换转义,也就是写两个%,以避免被误认为是占位符。使用str.format方法,也须要对花括号进行转义。
print('%.2f%%' % 12.5)print('{} replaces {{}}'.format(1.23))
输出结果以下:
12.50%1.23 replaces {}
在花括号内还能够指定传递给format方法的参数的位置索引,以用于替换占位符。这容许在不更改format方法传入值顺序的状况下,更改格式化字符串中占位符的顺序。
formatted = '{1} = {0}'.format(key, value)print(formatted)
输出结果以下所示:
1.234 = my_var
使用位置索引还有一个好处,就是在格式化字符串中要屡次引用某个值时,只须要经过format方法传递一个值便可。在格式化字符串中可使用同一个位置索引引用屡次这个值。
formatted = '{0} loves food. See {0} cook.'.format(name)print(formatted)
输出结果以下:
Max loves food. See Max cook.
不幸的是,format方法没法解决上面的问题2,因此在格式化以前须要对值进行小的修改时比较费劲(由于须要对齐参数的位置)。下面的代码是将%运算符和format方法在一块儿进行比较,其实同时一样不容易阅读。
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方法使用的格式化说明符还有更多高级选项,例如在占位符中使用字典键和列表索引的组合,以及将值强制转换为Unicode和repr字符串:
formatted = 'First letter is {menu[oyster][0]!r}'.format( menu=menu)print(formatted)
运行结果以下:
First letter is 'k'
可是这些功能并不能帮助减小上述问题4中重复key的冗余性。例如,在这里,我将在C风格格式化表达式中使用字典的冗长性与将key参数传递给format方法的新样式进行了比较:
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
这种样式的噪音较小,由于它消除了词典中的一些引号和格式化说明符中的一些字符,可是并无达到完美的程度。此外,在占位符中使用字典键和索引的高级功能仅提供了Python表达式功能的一小部分。这种缺少表现力的局限性使得它从整体上破坏了format方法的价值。
考虑到这些缺点以及仍然存在C风格格式化表达式的问题(上面的问题2和问题4),个人建议是尽可能避免使用str.format方法。了解格式化说明符(冒号以后的全部内容)中使用的新的迷你语言以及如何使用格式内置功能是很是重要的。
3. f-字符串
Python 3.6添加了插值格式化字符串(简称f字符串)来完全解决这些问题。这种新的语言语法要求您以f字符做为格式字符串的前缀,这相似于字节字符串以b字符做为前缀,以及原始(未转义的)字符串以r字符做为前缀。
f-字符串将格式字符串的表现力发挥到极致,经过彻底消除提供要格式化的键和值的冗余性,彻底解决了问题4。它们经过容许您引用当前Python范围中的全部变量做为格式化表达式的一部分来实现这一点:
key = 'my_var'value = 1.234
formatted = f'{key} = {value}'print(formatted)
输出结果以下:
my_var = 1.234
格式化的内置迷你语言中的全部相同选项均可以在f-字符串内占位符后的冒号后面使用,也能够相似于str.format方法将值强制转换为Unicode和repr字符串:
formatted = f'{key!r:<10} = {value:.2f}'print(formatted)
输出结果以下:
'my_var' = 1.23
在全部状况下,使用f-字符串进行格式化比使用带有%运算符和str.format方法的C风格格式化字符串进行格式化要短。在这里,我按照最短到最长的顺序显示了全部这些格式化方式,以便您能够轻松进行比较:
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}
print(f'f_string:{f_string}')print(f'c_tuple:{c_tuple}')print(f'str_args:{str_args}')print(f'str_kw:{str_kw}')print(f'c_dict:{c_dict}')
输出结果以下:
f_string:my_var = 1.23c_tuple:my_var = 1.23str_args:my_var = 1.23str_kw:my_var = 1.23c_dict:my_var = 1.23
f-字符串还能够将完整的Python表达式放在占位符括号内,经过对使用简明语法格式化的值进行小的修改,能够从根本上解决问题2。如今,使用C样式格式化和str.format方法花费多行的内容如今很容易放在一行上:
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)}')
输出结果以下:
#1: Avocados = 1#2: Bananas = 2#3: Cherries = 15
Python表达式也能够出如今格式化说明符选项中。例如,在这里我经过使用变量而不是将其硬编码为格式化字符串来指定要输出的浮点数位数:
places = 3number = 1.23456print(f'My number is {number:.{places}f}')
f-字符串可让表达力,简洁性和清晰度结合在一块儿,使它们成为Python程序员最好的内置选项。每当您发现本身须要将值格式化为字符串时,均可以选择f-字符串做为替代。

总结:
1. 使用%运算符的C风格格式化字符串会遇到各类陷阱和冗长的问题;
2.str.format方法在其格式说明符迷你语言中引入了一些有用的概念,但在其余方面会重复C风格格式化字符串的错误,应避免使用;
3. f-字符串是用于将值格式化为字符串的新语法,解决了C风格格式化字符串最大的问题;
4. f-字符串简洁而强大,由于它们容许将任意Python表达式直接嵌入格式说明符中;


对本文感兴趣,能够加李宁老师微信公众号(unitymarvel):


关注  极客起源  公众号,得到更多免费技术视频和文章。


本文分享自微信公众号 - 极客起源(geekculture)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。javascript

相关文章
相关标签/搜索