为什么Python 3.6 alpha中的字面格式化字符串(f-strings)如此缓慢?(现在在3.6稳定版中已经修复)

44

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

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

这似乎与在str实例上使用format函数做的事情相同。然而,我注意到这种字面字符串格式化与仅调用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 几乎花费了一半的时间。我本来预计字面方法会更快,因为涉及的语法更少。是什么导致了在幕后运行的使得字面方法如此之慢?


2
f-字符串是动态的,所以每次循环都必须生成该字符串;而格式化字符串是一个字面值,在代码运行之前就被创建,当它被转换为字节码时。 - PM 2Ring
@AlexHall 或许这与 x 在传递给 format 方法时被分配到了一个局部变量有关,但是在 f"..." 语法中必须在 globals 中找到它。 - user2390182
2
@AlexHall:这不是一个错误。实际上,在幕后有一个不同的实现,因为格式字符串必须在编译时解析,而str.format()运行时解析插槽。 - Martijn Pieters
1
@PM2Ring:所有表达式都在编译时编译并在运行时评估。 - Martijn Pieters
@MartijnPieters 如果字符串在运行时被编译,这意味着计算量会减少。至少如果 .format 更快,那么这些字符串应该简单地编译成对 .format 的调用。 - Alex Hall
显示剩余2条评论
2个回答

47
注意:本回答是针对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)操作码。新的FORMAT_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 Release Schedule以获取时间表,以及Python issue #27078(针对此问题开放)以讨论如何进一步提高格式化字符串字面量的性能。

为什么要使用 ''.join([...]) 而不是字符串拼接? - Alex Hall
3
由于字符串连接具有O(N ^ 2)的性能特征,因此建议避免使用A + B + C这样的方式。这种方式会先将A和B连接成一个新的字符串,然后再将结果与C拼接起来形成一个新的字符串。 - Martijn Pieters
2
@AlexHall:另一方面,字符串连接只需要计算最终字符串的大小,将A、B和C全部复制到其中。这是一个O(N)操作。 - Martijn Pieters
1
@AlexHall:字符串构建器仍然需要分配内存块以为附加数据腾出空间,然后生成最终的字符串对象。两种方法都有权衡之处。当您使用stringvar += otherstringstringvar = stringvar + otherstring时,CPython确实具有内部优化,但这是一项实现细节,也需要重新设计以支持此情况,因为这里实际上没有stringvar - Martijn Pieters
@ideasman42:嗯,在 alpha 和 beta 1 版本中还没有;-) 但最终版本的性能有了显著提升,是的。 - Martijn Pieters
显示剩余5条评论

38

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

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

然而,对于更复杂和更长的字符串,直接使用格式化字符串字面量仍然比相应的'...'.format(...)调用要快,因为后者在每次格式化字符串时都会解释该字符串。


这个问题正是提出issue 27078的主要动机,请求为字符串片段创建一个新的Python字节码opcode(该opcode得到一个操作数——堆栈上的片段数量;片段按出现顺序依次压入堆栈,即最后一部分是最上面的项目)。Serhiy Storchaka实现了这个新的opcode,并将其合并到CPython中,因此自Python 3.6 beta 1版本以来就可用(因此在Python 3.6.0正式版中也可用)。

结果,直接使用格式化字符串字面量比string.format要快得多。如果您只是插值strint对象,则它们通常比Python 3.6中的旧式格式化更快:

>>> 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 代码中的优化,彻底消除了六个低效源中的前五个。__format__ 方法仍未使用 slot 技术,因此需要对类进行字典查找,因此调用它比调用 __str__ 更慢,但在格式化 int 或 str 实例(而不是子类)时,现在可以完全避免调用。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接