CPython(引用解释器)对就地字符串连接进行了优化(当附加到的字符串没有其他引用时)。在执行+ 时,它不能可靠地应用此优化,只有+=(+ 涉及两个实时引用,赋值目标和操作数,前者不参与+ 操作,所以更难优化)。
不过,根据PEP 8,您不应该依赖这个:
代码的编写方式不应损害 Python 的其他实现(PyPy、Jython、IronPython、Cython、Psyco 等)。
例如,对于 a += b 或 a = a + b 形式的语句,不要依赖 CPython 对就地字符串连接的高效实现。即使在 CPython 中,这种优化也是脆弱的(它只适用于某些类型),并且在不使用引用计数的实现中根本不存在。在库的性能敏感部分,应该使用 ''.join() 形式。这将确保在各种实现中以线性时间发生连接。
根据问题编辑更新:是的,你破坏了优化。您连接了许多字符串,而不仅仅是一个,并且 Python 从左到右计算,所以它必须首先进行最左边的连接。因此:
new_file_content += line.strip() + "~" + row_region + "\n"
根本不一样:
new_file_content = new_file_content + line.strip() + "~" + row_region + "\n"
因为前者将所有 new 片段连接在一起,然后将它们一次全部附加到累加器字符串中,而后者必须使用不涉及 @ 的临时变量从左到右评估每个添加987654333@ 本身。为清楚起见添加括号,就像您所做的那样:
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 来说,积累是显而易见的,你可以解决性能问题。但坦率地说,只要您执行这样的逻辑附加操作,就应该只使用+=; += 的存在是有原因的,它为维护者和解释者提供了有用的信息。除此之外,就DRY 而言,这是一种很好的做法;为什么不需要命名变量两次?
当然,根据 PEP8 指南,即使在这里使用 += 也是错误的形式。在大多数具有不可变字符串的语言中(包括大多数非 CPython Python 解释器),重复的字符串连接是Schlemiel the Painter's Algorithm 的一种形式,这会导致严重的性能问题。正确的解决方案是构建一个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 类型本身(技术上,PyUnicode_Append 进行突变优化,但它需要解释器的帮助才能修正引用计数,以便它知道它可以安全地使用优化;没有解释器的帮助,只有 C 扩展模块会从优化中受益)。
当解释器 detects that both operands are the Python level str type(在 C 层,在 Python 3 中,它仍然被称为 PyUnicode,不值得更改的 2.x 天的遗留物)时,它调用 a special unicode_concatenate function,它检查下一条指令是否是三个基本STORE_* 指令之一。如果是,并且目标与左操作数相同,它将清除目标引用,因此 PyUnicode_Append 将只看到对该操作数的单个引用,从而允许它调用具有单个引用的 str 的优化代码.
这意味着你不仅可以通过这样做来打破优化
a = a + b + c
您也可以在相关变量不是顶级(全局、嵌套或本地)名称时打破它。如果您正在对对象属性、list 索引、dict 值等进行操作,即使 += 也无济于事,它不会看到“简单的STORE”,所以它不清除目标引用,所有这些都得到超慢、非就地行为:
foo.x += mystr
foo[0] += mystr
foo['x'] += mystr
它也特定于str 类型;在 Python 2 中,优化对 unicode 对象没有帮助,在 Python 3 中,它对 bytes 对象没有帮助,并且在这两个版本中都不会针对 str 的子类进行优化;那些总是走慢路。
基本上,对于 Python 新手来说,优化是在最简单的常见情况下尽可能好,但对于稍微复杂的情况,它也不会造成严重的麻烦。这只是加强了 PEP8 的建议:如果您可以通过做正确的事情并使用 str.join 在 每个 解释器上运行得更快,对于任何商店目标,依赖于解释器的实现细节是一个坏主意。