拥抱f-string
字符串从来就是计算机数据中最重要的一种基本类型。对于字符串,我们有各种各样的话题进行讨论。然而,字符串最基本的操作之一:打印和格式化,是最重要的话题之一。本文主要探讨Python3.6以来新增的字符串格式化方法f-string
,以及一些相关的内容。
阅读本文你需要:Python3.6.0及以上版本
0x01. 字符串插入与格式化
从C++和C等传统语言过来的人,很熟悉printf
这种函数族,也就是所谓的字符串格式化。这种字符串往往是由包含着格式化限定符(Format Specifier)的字符串字面量构成,后面跟上每一个限定符所对应的参数类型。取决于语言类型,编译器/解释器会在编译时或者运行时解析这样的字符串,然后按照格式把变量内容填充进去。
然而在Bash等类语言中,存在一种通用的字符串格式方法:String Interpolation,字符串插入。
传统意义上来说,往一个成型字符串字面量中后期插入一些数据都叫字符串插入,而对这些插入的内容修改显示格式才叫做字符串格式化。然而,在C++等语言中,字符串插入的同时会进行字符串格式化,所以我们叫做字符串格式化。在本文中,这两个词的含义一致。
比如,考虑如下的javascript代码:
|
|
可以看到,这样的字符串格式化易读性大大提高,而使用起来也变得更加简单。
Python于2.6引入了str.format()
(相关PEP为PEP 3101 – Advanced String Formatting)作为Python的字符串插入方法。不过,这样的字符串插入方法与上文中提到的可谓是大相径庭——我们需要写出模板字面量并手动调用format()
方法来制定替换元素,格式化字符串。
2015年,Eric V. Smith 提出了PEP 498 – Literial String Interpolation,在这个PEP中,Eric V. Smith详细分析了Python已有的三种字符串格式化方法:
%
格式化表达式:1
print('%s' % 'Hello, World!')
string.Template
:1 2 3
from string import Template temp = Template('$s') print(temp.substitute({'s': 'Hello, World!'}))
以及
str.format()
:1
print('{0}'.format('Hello, World!'))
这三种方法都有各自的缺点。
之后,Smith从编译器等角度分析,提出了f-string
,并针对技术细节给出了详细的描述。
两年后,f-string
被Python3.6版本正式吸纳为新的Feature。
0x02. 现有格式化方法的缺点
那么,Python现在存在了三种字符串格式化方法,有什么缺点呢?
难以阅读
考虑如下代码:
1
print("Hello %s. I'm %r and %d years old." % (name, my_name, my_ags))
对于一位有着C++或者Java经验的用户,这个原字符串或许并不会造成很多困难。然而,从第一眼来看,这个字符串想表达的内容仍然不够清晰,易读性并不高;同时,对于IDE或者Linter,也需要做一些额外的工作去分析这样的格式化语句。
无法扩展
对于
%
格式化表达式和string.Template
来说,我们无法针对特有行为进行扩展。比如对于如下一个类:1 2 3 4 5
class Person: def __init__(self, name, age, gender): self.name = name self.age = age self.gender = gender
如何使用
%
格式化表达式和string.Template
格式化输出当前这个类?当然,我们可以实现__repr__
和__str__
方法来”曲线救国“,但是这样和我们初衷就略有违背:假如对于某些特殊类我们需要单独的信息表示,然而在打印的时候我们需要其他的信息,那么我们自然会冲突。笨重
来自
str
的format()
系方法解决了上面两个问题。str.format()
本身就是一种String Interpolation,但是str.format()
的问题在于太过笨重,比如:1
print("Hello {name}. I'm {my_name} and {my_age} years old.".format(name='Ramen', my_name='Python', my_age=2018 - 1989))
虽然能够正常工作,但是这样整个方法的长度太长太笨重了,易读性也不强。
我们的f-string,就是为了解决以上这些缺点诞生的新字符串格式化方法。
0x03. f-string
所谓的f-string
,正式名称为Formatted string literals,格式化字符串字面量。也就是说,f-string
实际上和str.format()
中的原字符串一致,都是一种表示输出结构的模板,而区别在于,f-string
编译器会自动去parse和做替换,而普通的需要我们自己去调用str.format()
方法。
f-string
支持来自str.format()
定义的minimal language,也就是说,几乎所有原来的字符串都可以在字符串前加上一个f
来直接转换成字符串字面量。
同时,f-string
也支持__format__
协议,也就是说,可以对于任何自建类进行自定义格式化方法。
怎么使用f-string
?
很简单,普通字符串使用
f
做前缀修饰。如:1 2 3 4 5 6
name = 'Ramen' my_name = 'Python' my_age = 2018 - 1989 # f-string print(f"Hello {name}. I'm {my_name} and {my_age} years old.")
输出为:
1
Hello Ramen. I'm Python and 29 years old.
比如在一个自定义的
Vector
类中,我们可以这么用:(例子来源于《流畅的Python》第一章)1 2 3 4 5 6 7
class Vector: # ... def __repr__(self): return f'Vector({self.x}, {self.y})' # ...
这样就显得很清晰。
f-string
的内部是一个expression(支持完整的python expression),所以我们可以写出这样的调用:1 2 3 4
lst = [1, 2, 3] d = {'name': 'Ramen', 'foo': 'bar'} print(f"{lst[0]} {d['name']}")
输出为:
1
1 Ramen
同样,
f-string
支持来自于str.format()
中定义的minimal language:1 2 3
hex_represents = 0x1f2f print(f'The num is: {hex_represents:5.2f}')
输出为:
1
The num is: 7983.00
f-string
能够访问所有的变量:1 2 3 4 5 6 7 8 9
a = 15 def closure(num): def inner(): print(f'Global variable:{a} and free variable:{num}') return inner closure(42)()
f-string
访问到了外侧的free variable(num
)和 global variable(a
)。当然,生成式表达式也是支持的:
1 2 3
num = [1, 2, 3, 4, 5, 6] print(' '.join(f'{k}' for k in num))
输出为:
1
1 2 3 4 5 6
和
r
配合的顺序为:f-string
先进行内部的表达式求值以及替换,然后r
把字符串变换为原始字符串:1 2 3 4
x = 45 print(rf'x={x:x}\n') print(fr'x={x:x}\n')
输出为:
1 2
x=2d\n x=2d\n
最后是如何输出
{}
。在f-string
内部不允许使用反斜杠\
,所以不能这么输出一组{}
:1 2 3
num = 42 print(f'\{{num}\}')
运行结果为:
1
SyntaxError: f-string: single '}' is not allowed
正确用法为:
{{ }}
,如:1 2 3
num = 42 print(f'{{{num}}}')
输出为:
1
{42}
更多用法和形式化的句法参考等可以看官方文档。
0x04. When to use?
什么时候使用呢?
有强烈上下文关联的情况时:如上面的例子:
1 2 3 4 5 6 7
class Vector: # ... def __repr__(self): return f'Vector({self.x}, {self.y})' # ...
此时,
self.x
和self.y
为Vector
的两个坐标,意义十分清晰。几乎所有使用
%
格式化表达式的地方。可以大幅简化
str.format()
的情况时。
那什么时候不应该使用?
- 别名指代时。有些时候,我们希望在内使用别名指代,如使用
{version}
指代current_release_version
,而此时全局已经有一个version
了,此时自然不能使用f-string
去捕获全局的version
,而使用format(version=current_release_version)
不失为一个更好的方法。 - 制定规则替换时。这样的工作还是交给
string.Template
吧。 - 需要格式化
bytes
串时。f-string
并不支持bytes
串,这也意味着不能使用b
前缀和f
前缀配合。 - docstring。
f-string
不能被用作docstring。
这些只是部分情况,但是作为和str.format()
相辅相成的功能,应该保持如下一种态度:在必要的时候使用str.format()
和f-string
,尽量减少%
格式化表达式的使用,在需要的时候使用string.Template
。