为啥在 Python 3.6 alpha 中文字格式的字符串(f 字符串)如此缓慢? (现在在 3.6 稳定版中修复)

Posted

技术标签:

【中文标题】为啥在 Python 3.6 alpha 中文字格式的字符串(f 字符串)如此缓慢? (现在在 3.6 稳定版中修复)【英文标题】:Why were literal formatted strings (f-strings) so slow in Python 3.6 alpha? (now fixed in 3.6 stable)为什么在 Python 3.6 alpha 中文字格式的字符串(f 字符串)如此缓慢? (现在在 3.6 稳定版中修复) 【发布时间】:2016-09-18 19:38:14 【问题描述】:

我从 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 几乎花费了一半的时间。我希望文字方法更快,因为涉及的语法更少。幕后发生了什么导致文字方法慢得多?

【问题讨论】:

f 字符串是动态的,因此必须在每个循环中生成字符串;而格式字符串是在代码运行之前创建的文字,当它被转换为字节码时。 @AlexHall 也许这与x 在传递给format 方法时分配给局部变量的事实有关,但必须由@ 在globals 中找到987654333@ 语法。 @AlexHall:这不是错误。底层只是一个不同的实现,因为格式字符串必须在编译时解析,而 str.format()runtime 解析槽。 @PM2Ring:所有表达式在编译时编译并在运行时计算。 @MartijnPieters 如果字符串是在运行时编译的,那应该意味着更少的计算。至少如果.format 更快,那么这些字符串应该简单地编译成对.format 的调用。 【参考方案1】:

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

    它构建了一系列字符串片段... ... 这个序列是一个列表,而不是一个元组! (构造元组比构造列表要快一些)。 它将一个空字符串压入堆栈 它在空字符串上查找join 方法 它甚至在裸 Unicode 对象上调用 __format____format__('') 将始终返回 self,或整数对象,__format__('') 作为参数返回 str(self)__format__ 方法未插入。

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


这个问题是issue 27078 要求新的 Python 字节码操作码以将字符串片段转换为字符串的主要动机(操作码获取一个操作数 - 堆栈上的片段数;片段被推送到堆栈中出现的顺序,即最后一部分是最上面的项目)。 Serhiy Storchaka 实现了这个新的操作码并将其合并到 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 代码中的优化完全消除了6 个低效率源中的前5 个。 __format__ 方法仍然没有开槽,因此它需要对类进行字典查找,因此调用它必然比调用 __str__ 慢,但现在可以在格式化 int 的常见情况下完全避免调用或 str 实例(不是子类!)没有格式说明符。

【讨论】:

【参考方案2】:

注意:此答案是为 Python 3.6 alpha 版本编写的。 new opcode added to 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(针对此问题打开)讨论如何进一步提高格式化字符串文字的性能。

【讨论】:

非常好的解释,谢谢!我也不知道有一个__format__ 魔术方法。 为什么扩展为''.join([...])而不是字符串拼接? @AlexHall:因为字符串连接具有 O(N^2) 性能特征。 A + B + C 必须先为 A + B 创建一个字符串,然后将结果与 C 一起复制到一个新字符串中。 @AlexHall:另一方面,字符串连接只需要计算最终的字符串大小,将所有 A、B 和 C 复制到其中。这是一个 O(N) 操作。 @AlexHall:字符串构建器仍然需要分块分配内存以为额外数据腾出空间,然后生成最终的字符串对象。两种方法都需要权衡取舍。当您使用 stringvar += otherstringstringvar = stringvar + otherstring 时,CPython 确实有一个内部优化,但这是一个实现细节,也需要重新设计以支持这种情况,因为这里没有实际的 stringvar

以上是关于为啥在 Python 3.6 alpha 中文字格式的字符串(f 字符串)如此缓慢? (现在在 3.6 稳定版中修复)的主要内容,如果未能解决你的问题,请参考以下文章

为啥excel输入文字不自动换行

excel在一列前加相同的文字为啥前一列的文字会被覆盖

为啥这个正则表达式在最后一场比赛中有空格?

为啥excel中打字设置了自动换行有的文字却显示不出来,必须要双击才能看到全部文字

mac下安装2.7和3.6版本的Python

为 Python 3.6 元类提供 __classcell__ 示例