39

我从 Python Github 存储库下载了 Python 3.6 alpha 版本,我最喜欢的新功能之一是文字字符串格式化。它可以像这样使用:

>>> x = 2
>>> f"x is {x}"
"x is 2"

这似乎与在实例上使用该format函数做同样的事情。str但是,我注意到的一件事是,与仅调用format. 以下是timeit关于每种方法的说明:

>>> x = 2
>>> timeit.timeit(lambda: f"X is {x}")
0.8658502227130764
>>> timeit.timeit(lambda: "X is {}".format(x))
0.5500578542015617

如果我使用字符串作为timeit' 的参数,我的结果仍然显示模式:

>>> timeit.timeit('x = 2; f"X is {x}"')
0.5786435347381484
>>> timeit.timeit('x = 2; "X is {}".format(x)')
0.4145195760771685

如您所见,使用format几乎花费了一半的时间。我希望文字方法更快,因为涉及的语法更少。幕后发生了什么导致文字方法慢得多?

4

2 回答 2

41

注意:这个答案是为 Python 3.6 alpha 版本编写的。添加到 3.6.0b1的新操作码显着提高了 f-string 性能。


f"..."语法被有效地转换为str.join()对表达式周围文字字符串部分的操作,{...}并且表达式本身的结果通过object.__format__()方法传递(传递任何:..格式规范)。拆机时可以看到:

>>> import dis
>>> dis.dis(compile('f"X is {x}"', '', 'exec'))
  1           0 LOAD_CONST               0 ('')
              3 LOAD_ATTR                0 (join)
              6 LOAD_CONST               1 ('X is ')
              9 LOAD_NAME                1 (x)
             12 FORMAT_VALUE             0
             15 BUILD_LIST               2
             18 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             21 POP_TOP
             22 LOAD_CONST               2 (None)
             25 RETURN_VALUE
>>> dis.dis(compile('"X is {}".format(x)', '', 'exec'))
  1           0 LOAD_CONST               0 ('X is {}')
              3 LOAD_ATTR                0 (format)
              6 LOAD_NAME                1 (x)
              9 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             12 POP_TOP
             13 LOAD_CONST               1 (None)
             16 RETURN_VALUE

请注意该结果中的BUILD_LISTLOAD_ATTR .. (join)操作码。newFORMAT_VALUE将堆栈顶部加上一个格式值(在编译时解析出来)将它们组合在一个object.__format__()调用中。

因此,您的示例 ,f"X is {x}"被翻译为:

''.join(["X is ", x.__format__('')])

请注意,这需要 Python 创建一个列表对象,并调用该str.join()方法。

调用也是方法调用,str.format()解析后还有调用x.__format__(''),但关键是这里没有创建列表。正是这种差异使该str.format()方法更快。

请注意,Python 3.6 仅作为 alpha 版本发布;这个实现仍然可以很容易地改变。请参阅PEP 494 – Python 3.6 发布时间表以了解时间表,以及Python 问题 #27078(针对此问题打开)讨论如何进一步提高格式化字符串文字的性能。

于 2016-05-21T16:34:30.037 回答
33

在 3.6 beta 1 之前,格式字符串f'x is {x}'被编译为''.join(['x is ', x.__format__('')]). 由于以下几个原因,生成的字节码效率低下:

  1. 它构建了一系列字符串片段......
  2. ...这个序列是一个列表,而不是一个元组!(构造元组比列表略快)。
  3. 它将一个空字符串压入堆栈
  4. join它在空字符串上查找方法
  5. __format__它甚至在裸 Unicode 对象上调用,它__format__('')总是会返回self,或者整数对象,__format__('')作为参数返回str(self)
  6. __format__方法没有开槽。

但是,对于更复杂和更长的字符串,文字格式化的字符串仍然会比相应的'...'.format(...)调用更快,因为对于后者,每次格式化字符串时都会解释字符串。


这个问题是问题 27078的主要动机,要求将字符串片段转换为字符串的新 Python 字节码操作码(操作码获取一个操作数 - 堆栈上的片段数;片段按出现的顺序推入堆栈即最后一部分是最上面的项目)。Serhiy Storchaka 实现了这个新的操作码并将其合并到 CPython 中,这样它就可以在 Python 3.6 中使用,从 beta 1 版本开始(因此在 Python 3.6.0 最终版本中)。

结果,文字格式的字符串将比string.format. 如果您只是插值或对象,它们通常也比 Python 3.6 中的旧式格式快得多:strint

>>> timeit.timeit("x = 2; 'X is {}'.format(x)")
0.32464265200542286
>>> timeit.timeit("x = 2; 'X is %s' % x")
0.2260766440012958
>>> timeit.timeit("x = 2; f'X is {x}'")
0.14437875000294298

f'X is {x}'现在编译为

>>> dis.dis("f'X is {x}'")
  1           0 LOAD_CONST               0 ('X is ')
              2 LOAD_NAME                0 (x)
              4 FORMAT_VALUE             0
              6 BUILD_STRING             2
              8 RETURN_VALUE

BUILD_STRING的 以及FORMAT_VALUE代码优化完全消除了 6 个低效率源中的前 5 个。该__format__方法仍然没有开槽,因此它需要对类进行字典查找,因此调用它必然比调用慢__str__,但现在可以在格式化intstr实例(不是子类!)的常见情况下完全避免调用而不格式化说明符。

于 2016-09-06T20:43:59.280 回答