gun*_*gor 8 python string cpython string-concatenation python-internals
当我们使用时,我们的代码需要10分钟来虹吸68,000条记录:
new_file = new_file + line + string
Run Code Online (Sandbox Code Playgroud)
但是,当我们执行以下操作时,只需1秒钟:
new_file += line + string
Run Code Online (Sandbox Code Playgroud)
这是代码:
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))
Run Code Online (Sandbox Code Playgroud)
我在python中编写的所有代码都使用第一个选项.这只是基本的字符串操作......我们正在从文件读取输入,处理它并将其输出到新文件.我100%肯定第一种方法比第二种方法运行时间大约长600倍,但为什么呢?
正在处理的文件是csv,但使用〜而不是逗号.我们在这里所做的只是使用这个csv,它有一个国家列,并为国家地区添加一个列,例如LAC,EMEA,NA等... cmdbre.regions只是一个字典,所有~200个国家作为关键和每个区域的价值.
一旦我改为追加字符串操作......循环在1秒而不是10分钟内完成... csv中的68,000条记录.
Sha*_*ger 25
CPython(引用解释器)具有就地字符串连接的优化(当附加到的字符串没有其他引用时).这样做时,它不能作为可靠地应用这种优化的+
,只有+=
(+
包括两个活动的引用,分配对象和操作数,前者不参与+
经营,所以它很难去优化它).
根据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"
Run Code Online (Sandbox Code Playgroud)
与以下内容完全不同:
new_file_content = new_file_content + line.strip() + "~" + row_region + "\n"
Run Code Online (Sandbox Code Playgroud)
因为前者将所有新片段连接在一起,然后将它们一次性地附加到累加器字符串,而后者必须使用不涉及new_file_content
自身的临时值从左到右评估每个添加.为了清晰起见添加了parens,就像你做的那样:
new_file_content = (((new_file_content + line.strip()) + "~") + row_region) + "\n"
Run Code Online (Sandbox Code Playgroud)
因为在它到达它们之前它实际上并不知道类型,所以它不能假设所有这些都是字符串,所以优化不会起作用.
如果您将第二位代码更改为:
new_file_content = new_file_content + (line.strip() + "~" + row_region + "\n")
Run Code Online (Sandbox Code Playgroud)
或稍慢,但仍然比慢速代码快很多倍,因为它保持了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"
Run Code Online (Sandbox Code Playgroud)
所以CPython的积累是显而易见的,你可以解决性能问题.但坦率地说,你应该只是在+=
你执行这样的逻辑追加操作时使用; +=
存在是有原因的,它为维护者和解释者提供了有用的信息.除此之外,就DRY而言,这是一个很好的做法; 为什么在不需要的时候将变量命名为两次?
当然,根据PEP8指南,即使+=
在这里使用也是不好的形式.在大多数使用不可变字符串的语言中(包括大多数非CPython Python解释器),重复的字符串连接是Schlemiel the Painter算法的一种形式,它会导致严重的性能问题.正确的解决方案是构建一个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)
Run Code Online (Sandbox Code Playgroud)
即使CPython字符串连接选项可用,它通常也更快,并且在非CPython Python解释器上也可以快速地运行,因为它使用mutable 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)
Run Code Online (Sandbox Code Playgroud)
对于那些对实现细节感兴趣的人,CPython字符串concat优化是在字节码解释器中实现的,而不是在str
类型本身上实现(从技术上讲,PyUnicode_Append
是否进行了变异优化,但它需要解释器的帮助来修复引用计数,因此它知道它可以安全地使用优化;没有解释器帮助,只有C扩展模块才能从优化中受益).
当解释器检测到两个操作数都是Python级别str
类型时(在C层,在Python 3中,它仍然被称为PyUnicode
,2.x天的遗产,不值得更改),它调用一个特殊unicode_concatenate
函数,它检查下一条指令是否是三条基本STORE_*
指令之一.如果是,并且目标与左操作数相同,则清除目标引用,因此PyUnicode_Append
将只看到对操作数的单个引用,允许它为str
具有单个引用的a调用优化代码.
这意味着您不仅可以通过执行来打破优化
a = a + b + c
Run Code Online (Sandbox Code Playgroud)
如果有问题的变量不是顶级(全局,嵌套或本地)名称,您也可以将其分解.如果你正在操作一个对象属性,一个list
索引,一个dict
值等,甚至+=
不会帮助你,它就不会看到"简单STORE
",所以它不会清除目标引用,所有这些获得超低的,非就地行为:
foo.x += mystr
foo[0] += mystr
foo['x'] += mystr
Run Code Online (Sandbox Code Playgroud)
它也是特定的str
类型; 在Python 2中,优化对unicode
对象没有帮助,而在Python 3中,它对bytes
对象没有帮助,并且在两个版本中它都不会针对子类进行优化str
; 那些总是走慢路.
基本上,对于刚接触Python的人来说,最简单的常见情况下,优化是尽可能好的,但即使是中等复杂的情况也不会给它带来严重的麻烦.这只是加强了PEP8的建议:根据你的解释器的实现细节是一个坏主意,你可以通过做正确的事情和使用,在每个解释器,任何商店目标上运行得更快str.join
.
实际上,两者都可能同样慢,但对于一些优化实际上是官方Python运行时(cPython)的实现细节.
Python中的字符串是不可变的 - 这意味着当你执行"str1 + str2"时,Python必须创建第三个字符串对象,并将所有内容从str1和str2复制到它 - 无论这些部分有多大, .
inplace运算符允许Python使用一些内部优化,以便str1中的所有数据不必再次复制 - 甚至可能允许一些缓冲区空间用于进一步的连接选项.
当人们对语言的工作方式有所了解时,从小字符串构建大型文本体的方法是创建一个包含所有字符串的Python列表,并在循环结束后,对str.join
所有传入的方法进行一次调用字符串组件.即使在Python实现中也是如此,并且不依赖于能够被触发的优化.
output = []
for ...:
output.append(line)
new_file = "\n".join(output)
Run Code Online (Sandbox Code Playgroud)
归档时间: |
|
查看次数: |
1108 次 |
最近记录: |