当我们使用时,我们的代码需要10分钟来虹吸68,000条记录:
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中编写的所有代码都使用第一个选项。这只是基本的字符串操作......我们正在从文件读取输入,处理它并将其输出到新文件。我100%肯定第一种方法比第二种方法运行时间大约长600倍,但为什么呢?
正在处理的文件是csv,但使用〜而不是逗号。我们在这里所做的就是使用这个csv,它有一个国家列,并为国家/地区添加一列,例如LAC,EMEA,NA等... cmdbre.regions只是一本字典,以所有约200个国家为关键,每个区域为价值。
一旦我改为追加字符串操作......循环在1秒而不是10分钟内完成... csv中的68,000条记录。
答案 0 :(得分:25)
CPython(引用解释器)具有就地字符串连接的优化(当附加到的字符串没有其他引用时)。在执行+
时,它无法可靠地应用此优化,只有+=
(+
涉及两个实时引用,分配目标和操作数,前者不是{&1;}。参与+
操作,因此更难以优化它。)
根据PEP 8:
,您不应该依赖此代码的编写应该不会影响Python的其他实现(PyPy,Jython,IronPython,Cython,Psyco等)。
例如,不要依赖CPython对a + = b或a = a + b形式的语句的就地字符串连接的有效实现。这种优化即使在CPython中也很脆弱(它仅适用于某些类型),并且在不使用引用计数的实现中根本不存在。在库的性能敏感部分中,应使用' .join()表单。这将确保连接在各种实现中以线性时间发生。
根据问题修改进行更新:是的,您打破了优化。你连接了许多字符串,而不仅仅是一个字符串,Python从左到右进行评估,所以它必须先进行最左边的连接。因此:
new_file_content += line.strip() + "~" + row_region + "\n"
与以下内容完全不同:
new_file_content = new_file_content + line.strip() + "~" + row_region + "\n"
因为前者将所有 new 块连接在一起,然后将它们一次性地附加到累加器字符串,而后者必须从左到右评估每个添加的临时值。涉及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的积累是显而易见的,你可以解决性能问题。但坦率地说,只要您执行像这样的逻辑追加操作,就应该使用+=
; +=
存在是有原因的,它为维护者和解释者提供了有用的信息。除此之外,就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
来有效地累积结果,然后允许{{1预计算字符串的总长度,一次性分配最终字符串(而不是沿途增量调整大小),并将其填充一次。
旁注:根据您的具体情况,您根本不应该累积或连接。您已获得输入文件和输出文件,并且可以逐行处理。每次你要追加或累积文件内容时,只需将它们写出来(我已经清理了一些代码,以便在进行PEP8时遵循PEP8和其他小的风格改进):
''.join
对于对实现细节感兴趣的人,CPython字符串concat优化是在字节码解释器中实现的,而不是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)
类型本身(技术上,str
进行了突变优化,但它需要帮助从解释器修复引用计数,因此它知道它可以安全地使用优化;没有解释器帮助,只有C扩展模块才能从优化中受益)。
当翻译detects that both operands are the Python level str
type(在C层,在Python 3中,它仍然被称为PyUnicode_Append
时,2.x天的遗产不值得改变),它调用a special unicode_concatenate
function,它检查下一条指令是否是三条基本PyUnicode
指令之一。如果是,并且目标与左操作数相同,则清除目标引用,因此STORE_*
将只看到对操作数的单个引用,允许它调用PyUnicode_Append
的优化代码只有一个参考。
这意味着您不仅可以通过
来打破优化str
如果有问题的变量不是顶级(全局,嵌套或本地)名称,您也可以将其分解。如果您正在对某个对象属性,a = a + b + c
索引,list
值等进行操作,即使dict
也无法帮助您,它也会赢得'看到"简单+=
",所以它没有清除目标参考,所有这些都得到超低的非就地行为:
STORE
它也特定于foo.x += mystr
foo[0] += mystr
foo['x'] += mystr
类型;在Python 2中,优化对str
个对象没有帮助,而在Python 3中,它对unicode
个对象没有帮助,并且在两个版本中它都不会优化子类的bytes
;那些总是走慢路。
基本上,对于刚接触Python的人来说,最简单的常见情况下,优化是尽可能好的,但即使是中等复杂的情况也不会给它带来严重的麻烦。这只是强调了PEP8的建议:通过做正确的事情并使用{{1},当你可以在每个解释器,任何商店目标上运行得更快时,根据你的解释器的实现细节是个坏主意。 }。
答案 1 :(得分:7)
实际上,两者都可能同样慢,但对于某些优化而言,这实际上是官方Python运行时(cPython)的实现细节。
Python中的字符串是不可变的 - 这意味着当你执行“str1 + str2”时,Python必须创建第三个字符串对象,并将所有内容从str1和str2复制到它 - 无论任何大小这些部分是。
inplace运算符允许Python使用一些内部优化,以便str1中的所有数据不必再次复制 - 甚至可能允许一些缓冲区空间用于进一步的连接选项。
当人们对语言的工作方式有所了解时,从小字符串构建大型文本体的方法是创建一个包含所有字符串的Python列表,在循环结束后,只需调用{传递所有字符串组件的{1}}方法。即使在Python实现中,这也将一直很快,并且不依赖于能够被触发的优化。
str.join