gpt4 book ai didi

python - 为什么'new_file + = line + string'比'new_file = new_file + line + string'快得多?

转载 作者:太空狗 更新时间:2023-10-29 20:32:59 26 4
gpt4 key购买 nike

这个问题已经有了答案:
Why is variable1 += variable2 much faster than variable1 = variable1 + variable2?
1个答案
当我们使用以下代码时,我们的代码需要10分钟来虹吸68000条记录:

new_file = new_file + line + string

但是,当我们执行以下操作时,只需1秒钟:
new_file += line + string

代码如下:
for line in content:
import time
import cmdbre

fname = "STAGE050.csv"
regions = cmdbre.regions
start_time = time.time()
with open(fname) as f:
content = f.readlines()
new_file_content = ""
new_file = open("CMDB_STAGE060.csv", "w")
row_region = ""
i = 0
for line in content:
if (i==0):
new_file_content = line.strip() + "~region" + "\n"
else:
country = line.split("~")[13]
try:
row_region = regions[country]
except KeyError:
row_region = "Undetermined"
new_file_content += line.strip() + "~" + row_region + "\n"
print (row_region)
i = i + 1
new_file.write(new_file_content)
new_file.close()
end_time = time.time()
print("total time: " + str(end_time - start_time))

我用Python编写的所有代码都使用第一个选项。这只是基本的字符串操作…我们从一个文件中读取输入,对其进行处理并将其输出到新文件中。我百分之百肯定第一种方法的运行时间大约是第二种方法的600倍,但为什么呢?
正在处理的文件是csv,但使用~而不是逗号。我们在这里所做的就是取这个csv,它有一个国家列,并为国家区域添加一个列,例如lac、emea、na等…cmdbre.regions只是一个字典,以大约200个国家为键,每个地区为值。
一旦我改为附加字符串操作…循环在1秒而不是10分钟内完成…csv中有68000条记录。

最佳答案

cpython(引用解释器)对就地字符串连接进行了优化(当附加到的字符串没有其他引用时)。在执行+时,它不能可靠地应用此优化,只有+=+涉及两个实时引用,即赋值目标和操作数,前者不涉及+操作,因此很难对其进行优化)。
不过,你不应该依赖于这一点,根据:
代码的编写方式不应影响其他Python实现(pypy、jython、ironpython、cython、psyco等)。
例如,不要依赖于cpython对形式为a+=b或a=a+b的语句的就地字符串连接的有效实现。即使在cpython中,这种优化也是脆弱的(它只适用于某些类型),并且在不使用refcounting的实现中根本不存在。在库的性能敏感部分中,应改用“”join()窗体。这将确保连接在不同实现之间以线性时间发生。
基于问题编辑的更新:是的,你打破了优化。您连接了多个字符串,而不仅仅是一个,而且python的计算结果是从左到右的,所以它必须首先进行最左边的连接。因此:

new_file_content += line.strip() + "~" + row_region + "\n"

完全不同于:
new_file_content = new_file_content + line.strip() + "~" + row_region + "\n"

因为前者将所有新的部分连接在一起,然后将它们全部附加到累加器字符串中,而后者必须使用不涉及 new_file_content本身的临时性从左到右评估每个加法。为了清晰起见,添加parens,就像您做的那样:
new_file_content = (((new_file_content + line.strip()) + "~") + row_region) + "\n"

因为在到达类型之前它实际上并不知道这些类型,所以它不能假定所有这些类型都是字符串,所以优化不会开始。
如果您将代码的第二位更改为:
new_file_content = new_file_content + (line.strip() + "~" + row_region + "\n")

或者稍微慢一点,但仍然比慢代码快很多倍,因为它保持了cpython优化:
new_file_content = new_file_content + line.strip()
new_file_content = new_file_content + "~"
new_file_content = new_file_content + row_region
new_file_content = new_file_content + "\n"

所以积累对cpython来说是显而易见的,你可以解决性能问题。但坦率地说,在执行这样的逻辑附加操作时,您应该只使用 +=;存在 +=是有原因的,它为维护人员和解释器提供了有用的信息。除此之外,对于 PEP 8来说,这是一个很好的实践;当您不需要时,为什么要将变量命名两次?
当然,根据PEP8指南,即使使用 +=这里也是不好的形式。在大多数具有不可变字符串的语言中(包括大多数非cpython python解释器),重复的字符串连接是一种形式的 DRY,这会导致严重的性能问题。正确的解决方案是构建一个字符串的 list,然后一个字符串的 join,例如:
    new_file_content = []
for i, line in enumerate(content):
if i==0:
# In local tests, += anonymoustuple runs faster than
# concatenating short strings and then calling append
# Python caches small tuples, so creating them is cheap,
# and using syntax over function calls is also optimized more heavily
new_file_content += (line.strip(), "~region\n")
else:
country = line.split("~")[13]
try:
row_region = regions[country]
except KeyError:
row_region = "Undetermined"
new_file_content += (line.strip(), "~", row_region, "\n")

# Finished accumulating, make final string all at once
new_file_content = "".join(new_file_content)

它通常更快,即使cpython字符串连接选项可用,并且在非cpython python解释器上也会可靠地更快,因为它使用可变的 list有效地积累结果,然后允许 ''.join预计算字符串的总长度,分配一次生成最后一个字符串(而不是一路递增调整大小),并只填充一次。
旁注:对于您的特定情况,您根本不应该累积或连接。您有一个输入文件和一个输出文件,可以逐行处理。每次您要附加或积累文件内容时,只需将它们写出来(我已经清理了一点代码,以满足PEP8的遵从性和其他一些小的样式改进,而我正在进行这些改进):
start_time = time.monotonic()  # You're on Py3, monotonic is more reliable for timing

# Use with statements for both input and output files
with open(fname) as f, open("CMDB_STAGE060.csv", "w") as new_file:
# Iterate input file directly; readlines just means higher peak memory use
# Maintaining your own counter is silly when enumerate exists
for i, line in enumerate(f):
if not i:
# Write to file directly, don't store
new_file.write(line.strip() + "~region\n")
else:
country = line.split("~")[13]
# .get exists to avoid try/except when you have a simple, constant default
row_region = regions.get(country, "Undetermined")
# Write to file directly, don't store
new_file.write(line.strip() + "~" + row_region + "\n")
end_time = time.monotonic()
# Print will stringify arguments and separate by spaces for you
print("total time:", end_time - start_time)

实施细节深入研究
对于那些对实现细节感兴趣的人来说,cpython字符串concat优化是在字节代码解释器中实现的,而不是在类型本身上实现的(从技术上讲, str是进行突变优化的,但它需要解释器的帮助来修复引用计数,以便知道它可以使用优化是安全的;没有解释程序的帮助,只有C扩展模块才能从优化中受益)。
当解释器 Schlemiel the Painter's Algorithm(在c层,在python 3中,它仍然被称为 PyUnicode_Append,2.x天的遗留值,不值得更改),它调用 detects that both operands are the Python level str type,检查下一条指令是否是三条基本的 PyUnicode指令之一。如果是,并且目标与左操作数相同,则会清除目标引用,因此 unicode_concatenate将只看到对操作数的单个引用,从而允许它使用单个引用调用针对 STORE_*的优化代码。
这意味着不仅可以通过执行
a = a + b + c

您也可以在所讨论的变量不是顶级(全局、嵌套或本地)名称时中断它。如果您正在操作对象属性、 PyUnicode_Append索引、 str值等,即使 list对您也没有帮助,它也不会看到“simple dict”,因此它不会清除目标引用,所有这些都会得到超流而不是就地行为:
foo.x += mystr
foo[0] += mystr
foo['x'] += mystr

它还特定于 +=类型;在python 2中,优化对 STORE对象没有帮助;在python 3中,优化对 str对象没有帮助,在这两个版本中,它都不会对 unicode的子类进行优化;这些子类总是走慢路径。
基本上,对于刚接触Python的人来说,在最简单的常见情况下,优化是尽可能好的,但是对于更为复杂的情况,优化不会带来严重的麻烦。这就加强了PEP8的建议:根据您的解释器的实现细节,当您可以通过正确的操作和使用 bytes在每个解释器、任何存储目标上更快地运行时,这是一个坏主意。

关于python - 为什么'new_file + = line + string'比'new_file = new_file + line + string'快得多? ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40996801/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com