阅读本文你需要:Python3.6.0及以上版本

字符串从来就是计算机数据中最重要的一种基本类型。对于字符串,我们有各种各样的话题进行讨论。然而,字符串最基本的操作之一:打印和格式化,是最重要的话题之一。本文主要探讨Python3.6以来新增的字符串格式化方法f-string,以及一些相关的内容。

0x01. 字符串插入与格式化

从C++和C等传统语言过来的人,很熟悉printf这种函数族,也就是所谓的字符串格式化。这种字符串往往是由包含着格式化限定符(Format Specifier)的字符串字面量构成,后面跟上每一个限定符所对应的参数类型。取决于语言类型,编译器/解释器会在编译时或者运行时解析这样的字符串,然后按照格式把变量内容填充进去。

然而在Bash等类语言中,存在一种通用的字符串格式方法:String Interpolation,字符串插入。

传统意义上来说,往一个成型字符串字面量中后期插入一些数据都叫字符串插入,而对这些插入的内容修改显示格式才叫做字符串格式化。然而,在C++等语言中,字符串插入的同时会进行字符串格式化,所以我们叫做字符串格式化。在本文中,这两个词的含义一致。

比如,考虑如下的javascript代码:

1
2
3
4
var name = 'Ramen';
var age = 20;
console.log(`My name is ${name} and ${age} years old.`)
// My name is Ramen and 20 years old.

可以看到,这样的字符串格式化易读性大大提高,而使用起来也变得更加简单。

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-stringPython3.6版本正式吸纳为新的Feature。

0x02. 现有格式化方法的缺点

那么,Python现在存在了三种字符串格式化方法,有什么缺点呢?

  1. 难以阅读

    考虑如下代码:

    1
    
    print("Hello %s. I'm %r and %d years old." % (name, my_name, my_ags))

    对于一位有着C++或者Java经验的用户,这个原字符串或许并不会造成很多困难。然而,从第一眼来看,这个字符串想表达的内容仍然不够清晰,易读性并不高;同时,对于IDE或者Linter,也需要做一些额外的工作去分析这样的格式化语句。

  2. 无法扩展

    对于%格式化表达式和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__方法来”曲线救国“,但是这样和我们初衷就略有违背:假如对于某些特殊类我们需要单独的信息表示,然而在打印的时候我们需要其他的信息,那么我们自然会冲突。

  3. 笨重

    来自strformat()系方法解决了上面两个问题。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.xself.yVector的两个坐标,意义十分清晰。

  • 几乎所有使用%格式化表达式的地方。

  • 可以大幅简化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