在循环内重复添加字符串时,为什么 f 字符串比字符串连接慢?

Chr*_*els 6 python string loops concatenation f-string

我正在使用 timeit 对项目的一些代码进行基准测试(使用免费的 replit,因此需要 1024MB 内存):

\n
code = \'{"type":"body","layers":[\'\n\nfor x, row in enumerate(pixels):\n    for y, pixel in enumerate(row):\n        if pixel != (0, 0, 0, 0):\n            code += f\'\'\'{{"offsetX":{-start + x * gap},"offsetY":{start - y * gap},"rot":45,"size":{size},"sides":4,"outerSides":0,"outerSize":0,"team":"{\'#%02x%02x%02x\' % (pixel[:3])}","hideBorder":1}},\'\'\'\n    \ncode += \'],"sides":1,"name":"Image"}}\n
Run Code Online (Sandbox Code Playgroud)\n

该循环针对给定图像内的每个像素运行(当然效率不高,但我还没有实现任何减少循环时间的方法),因此我可以在循环中获得的任何优化都是值得的。

\n

我记得只要你组合 3 个以上的字符串\xe2\x80\x94,f 字符串就比字符串连接更快,如图所示,我组合了超过3个字符串\xe2\x80\x94,所以我决定将循环内的 += 替换为 f 字符串并查看改进。

\n
code = \'{"type":"body","layers":[\'\n\nfor x, row in enumerate(pixels):\n    for y, pixel in enumerate(row):\n        if pixel != (0, 0, 0, 0):\n            code = f\'\'\'{code}{{"offsetX":{-start + x * gap},"offsetY":{start - y * gap},"rot":45,"size":{size},"sides":4,"outerSides":0,"outerSize":0,"team":"{\'#%02x%02x%02x\' % (pixel[:3])}","hideBorder":1}},\'\'\'\n    \ncode += \'],"sides":1,"name":"Image"}}\n
Run Code Online (Sandbox Code Playgroud)\n

500次timeit迭代的结果:

\n
+= took 5.399778672000139 seconds\nfstr took 6.91279206800027 seconds\n
Run Code Online (Sandbox Code Playgroud)\n

我已经重新运行了多次;以上次数是迄今为止 f 弦所做的最好的次数。为什么在这种情况下 f 字符串速度较慢?

\n

PS:这是我第一次在这里发帖提问。任何有关如何改进我未来问题的建议将不胜感激:D

\n

Sha*_*ger 5

因此,首先,理论上,具有不可变字符串的语言中的重复串联是O(n\xc2\xb2),而有效实现的批量串联是O(n),因此从理论上讲,这两个版本的代码都不适合重复串联。适用于任何地方的工作版本O(n)是:

\n
code = [\'{"type":"body","layers":[\']  # Use list of str, not str\n\nfor x, row in enumerate(pixels):\n    for y, pixel in enumerate(row):\n        if pixel != (0, 0, 0, 0):\n            code.append(f\'\'\'{{"offsetX":{-start + x * gap},"offsetY":{start - y * gap},"rot":45,"size":{size},"sides":4,"outerSides":0,"outerSize":0,"team":"{\'#%02x%02x%02x\' % (pixel[:3])}","hideBorder":1}},\'\'\')  # Append each new string to list\n    \ncode.append(\'],"sides":1,"name":"Image"}}\')\ncode = \'\'.join(code)  # Efficiently join list of str back to single str\n
Run Code Online (Sandbox Code Playgroud)\n

您的代码+=恰好能够足够高效地工作,因为在连接到没有其他活动引用的字符串时,CPython 对字符串连接进行了特定的优化,但 PEP8 风格指南中的第一个编程建议特别警告不要依赖它

\n
\n

a += b...不要依赖 CPython\xe2\x80\x99s 有效实现或形式的语句的就地字符串连接a = a + b。即使在 CPython 中,这种优化也是脆弱的(它仅适用于某些类型),并且在不使用引用计数的实现中根本不存在 xe2x80x99。在库的性能敏感部分,\'\'.join()应改用表单。这将确保在各种实现中串联在线性时间内发生。

\n
\n

本质上,基于原始的+=代码受益于优化,因此最终执行的数据副本更少。基于 f 字符串的代码做了相同的工作,但在某种程度上阻止了 CPython 优化的应用(str 每次都构建一个全新的、越来越大的)。这两种方法的形式都很糟糕,其中一种在 CPython 上的表现稍微好一些。当您的热代码执行重复串联时,您已经在做错误的事情,只需在末尾使用listofstr和即可。\'\'.join

\n