编写高质量Python(第4条)用支持插值的 f-string 取代 C 风格的格式字符串与 str.format 方法

格式化是指把数据填写到预先定义的文本模板里面,形成一条用户可读的消息,并把这条消息保存成字符串的过程。用 Python 处理字符串有四种方法可以考虑,这些方法都内置在语言和标准库里面。

  • Python里面最常用的字符串格式化方式是采用 % 格式操作符

    这种操作符左边的文本模版叫做格式字符串,我们可以在操作符右边写上某个值或者多个值所构成的元祖,用来替换格式字符串里的相关符号

    例如,下面程序可以将难以阅读的二进制和十六进制转为可读的十进制

    a = 0b10111011
    b = 0x5cf
    print('Binary is %d, hex is %d' %(a, b))
    
    >>>
    Binary is 187,hex is 3167
    

格式字符串里面可以出现 %d 这样的格式说明符,这些说明符的意思是,% 右边的对应数值会以这样的格式来替换这一部分内容,跟C语言类似,常见的 print f选项都可以当成 Python 的格式说明符来用,例如 %s、%x、%f等,此外还可以控制小数点的位数,并指定填充和对齐方式。

​ 像这种C语言类型的格式字符串,在 Python 中有四个缺点。

第一个缺点是,如果 % 右侧那个元祖里面的值在类型或顺序上有变化,那么程序可能会因为转化类型不兼容问题而出现错误

例如,下面这个简单的格式化表达是正确的。

key = 'my_var'
value = 1.234
foramtted = '%-10s = %.2f' %(key, value)
print(formatted)

>>>
my_var = 1.23

​ 如果将key和value调换顺序,那么程序就会在运行时出现异常。

foramtted = '%-10s = %.2f' % (value, key)

>>>
Traceback ...
TypeError: must be real number, not str

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

recorded_string = '%.2f = %-10s' % (key,value)

>>>
Traceback ...
TypeError: must be real number, not str

要想避免这种问题,必须经常检查%操作符左右两侧的写法是否相互兼容。

第二个缺点是,在填充玩模版之前,经常要先对准备填写进去的这个值稍微做一些处理。

例如,下列代码用来罗列厨房里面的各种食材,现在那种想法并没有对填入的格式字符串里面的那三个值预先做出调整。

pantry = [
	('adocados', 1.25),
	('bananas', 2.5),
	('cheeries', 15),
]
for i,  (item,count) in enumerate(pantry):
	print('#%d: %-10s = %.2f' %(i, item, count))
	
>>>
#0: avocados  = 1.25
#1: banans    = 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

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

tempalte  = "%s love food. See %s cook."
name = "Max"
formatted = template %(name,name)
print(formatted)

>>>
Max loves food.See Max cook.

如果想在填充之前把这个值修改一下,那么必须同时修改多处才行。

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

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

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

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 的写法对比一下,可以更明确地意识到这种写法的缺点。

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

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

soup = 'lentil'
formatted = 'Today\'s soup is %(soup)s.' % {'soup': soup}
print(formatted)

>>>
Today's soup is lentil.

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

meau = {
	'soup': 'lentil',
	'oyster': 'kumamoto',
	'special': 'schnitzel',
}
template = ('Today\'s soup is %(soup)s,'
						'buy one get two %(oyster)s oysters,'
						'and our special entree is %(special)s.')
formatted = template % meau
print(formatted)

>>>
Today's soup is lentil,buy one get two kumamoto oysters,and our special entree is schnitzel.
  • 内置的format函数与str类的format方法

    Python3 添加了高级字符串格式化机制,它的表达能力比老式 C 风格的格式字符串要强,且不需要使用 % 字符串。我们针对需要调整格式的这个 Python 值,调用内置的 format 函数,并且把这个值所应具备的格式也传给该函数,即可实现格式化。下面这段代码,演示了这种新的格式化方式。在传给 format 函数的格式里面,逗号表示显示出千位分隔符,^表示居中对齐。

    a = 1234.5678
    formatted = format(a, ',.2f')
    print(formatted)
    
    b = 'my_string'
    formatted = format(b, '^20s')
    print('*', formatted, '*')
    
    >>>
    1,234.57
    *      my_string       *
    

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

    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 里面的 {、} 照原样输出,那么也得转义。

    print('%.2f%%' % 12.5)
    print('{} replace {{}}'.format(1.23))
    
    >>>
    12.50%
    1.23 replace {}
    

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

    formatted = '{1} = {0}'.format(key, value)
    print(formatted)
    
    >>>
    1.234 = my_var
    

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

    formatted = '{0} loves food. See {0} cook.'.fomat(name)
    print(formatted)
    
    >>>
    Max loves food. See Max cook.
    

    ​ 然而,这个新的 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))
    		assert old_style == new_style
    

    ​ 当然,这种{}形式的说明符,还是支持一些比较高级的用法,例如可以查询 dict 中某个键的值,可以访问 list 里某个位置的元素,还可以把值转化成 Unicode 或 repr 字符串。下面程序把这三个特点联系到了一起。

    formatted = 'First letter is {meau[oyster][0]!r}'.format(
    	menu = menu)
    print(formatted)
    
    >>>
    First letter is 'k'
    

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

    old_template = (
    		'Todat\' soup is %(soup)s,'
    		'buy one get two %(oyster)s oysters,'
    		'and our special entree is %(special)s.')
    old_formatted = old_template % {
      	'soup': 'lentil',
      	'oyster': 'kumamoto',
      	'special': 'schnitzel',
    }
    
    new_template = (
    		'Today\' soup is {soup},'
    		'buy one get two {oyster} oysters,'
    		'and our special entree is {special}.')
    new_formatted = new_template.format(
    			soup = 'lentil',
    			oyster = 'kumamoto',
    			special = 'schnitzel',)
    
    print(old_formatted)
    print(new_formatted)
    
    >>>
    Todat' soup is lentil,buy one get two kumamoto oysters,and our special entree is schnitzel.
    Today' soup is lentil,buy one get two kumamoto oysters,and our special entree is schnitzel.
    

    ​ 新写法稍微好一点,因为它不用定义 dict 了,所以不需要把键名用 ‘’ 给括起来。它的说明符也比旧写法的说明符要简单一些。然而这些优点还不突出。另外,虽然我们在新的写法里面,可以访问字典的键,也可以访问列表的元素,但这些功能只涵盖 Python表达式的一个小部分特性,str.format 方法还是没有能够把 Python 表达式的优势充分发挥出来。

    ​ 因为 str.format 方法有这样的一些缺点,而且没办法解决早前提到的第二个和第四个缺点,所以总体来说,不推荐 str.format方法。

  • 插值格式字符串

    Python3.6 添加了新特性,叫做插值字符串(简称 f - string),可以解决上述所有问题。新语法特性要求在格式字符串前面添加字母f作为前缀,这跟字母 b 和字母 r 的用法类似,也就是分别表示字节形式的字符串与原始的字符串作为前缀。

    ​ f - string 把格式字符串的表达能力发挥到了极致,它彻底解决了上文提到的第四个缺点,也就是键名重复导致的程序冗余问题。可以直接使用 f - string 的 {} 里面饮用当前 Python 范围内的所有名称。

    key = 'my_var'
    value = 1.234
    
    fomatted = f'{key} = {value}'
    print(formatted)
    
    >>>
    my_var = 1.234
    

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

    formatted = f'{key!r:<10} = {value:.2f}'
    print(formatted)
    
    >>>
    'my_var'       = 1.23
    

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

    f_string = f'{key:<10} = {value:.2f}'
    
    c_cuple  = '%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 表达式都可以出现在{}里,于是这就解决了前面提到的第二个缺点。我们现在可以用相当简洁的写法对需要填充到字符串的值进行微调。

    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
    

    ​ Python表达式也可以出现在格式说明符中。例如,下面的代码把小数点之后的位数用变量来进行表示,然后把这个变量的名字 places 用{}括起来,由此以来代码更加灵活。

    places = 3
    number = 1.23456
    print(f'My number is {number:.{places}f}')
    
    >>>
    My number is 1.235
    

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值