为什么“new_file+=line+string”比“new_file=new_file+line+string”快得多?

2024-09-29 23:29:33 发布

您现在位置:Python中文网/ 问答频道 /正文

当我们使用以下代码时,我们的代码需要10分钟来虹吸68000条记录:

new_file = new_file + line + string

但是,当我们执行以下操作时,只需1秒:

^{pr2}$

代码如下:

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,它有一个国家的列,并添加一个国家地区的列,例如拉丁美洲和加勒比海、欧洲、中东和非洲、北美等。。。cmdbre区域只是一本字典,以所有~200个国家为关键字,每个地区为值。在

一旦我改为追加字符串操作。。。循环在1秒内完成,而不是10分钟。。。csv中有68000条记录。在


Tags: csv代码innewfortime记录line
2条回答

实际上,两种方法的速度都一样慢,但对于某些优化来说,这实际上是官方Python运行时(cPython)上的实现细节。在

Python中的字符串是不可变的,这意味着当您执行“str1+str2”操作时,Python必须创建第三个string对象,并将str1和str2中的所有内容复制到它身上,无论这些部分有多大。在

inplace操作符允许Python使用一些内部优化,这样str1中的所有数据都不必再被复制一次,甚至可能还允许一些缓冲区空间用于进一步的连接选项。在

当人们了解了语言的工作原理后,从小字符串构建大文本体的方法是用所有字符串创建一个Python列表,循环结束后,对传入所有字符串组件的str.join方法进行一次调用。即使是在Python实现中,这一速度也会一直很快,而且不依赖于能够触发的优化。在

output = []
for ...:
    output.append(line)

new_file = "\n".join(output)

CPython(引用解释器)对就地字符串连接进行了优化(当附加到的字符串没有其他引用时)。在执行+时,它无法可靠地应用此优化,只有+=+涉及两个实时引用,赋值目标和操作数,而前者不涉及{}操作,因此更难对其进行优化)。在

但您不应该依赖于此,根据PEP 8

Code should be written in a way that does not disadvantage other implementations of Python (PyPy, Jython, IronPython, Cython, Psyco, and such).

For example, do not rely on CPython's efficient implementation of in-place string concatenation for statements in the form a += b or a = a + b . This optimization is fragile even in CPython (it only works for some types) and isn't present at all in implementations that don't use refcounting. In performance sensitive parts of the library, the ''.join() form should be used instead. This will ensure that concatenation occurs in linear time across various implementations.

根据问题编辑进行更新:是的,您破坏了优化。连接了许多字符串,而不仅仅是一个字符串,Python从左到右求值,因此它必须首先执行最左边的连接。因此:

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

完全不同于:

^{pr2}$

因为前者将所有的新的片段连接在一起,然后一次将它们附加到累加器字符串中,而后者必须使用不涉及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指南,即使在这里使用+=也是不好的形式。在大多数具有不可变字符串的语言中(包括大多数非cpythonpython解释器),重复的字符串连接是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 ^{} type(在C层,在Python3中,它仍然被称为PyUnicode,这是一个2.x天的遗留问题,不值得更改),它调用a special ^{} function,它检查下一条指令是否是三条基本STORE_*指令之一。如果是,并且目标与左操作数相同,则会清除目标引用,这样PyUnicode_Append将只看到对操作数的单个引用,从而允许它使用单个引用调用str的优化代码。在

这意味着不你不仅可以通过

a = a + b + c

当相关变量不是顶级(全局、嵌套或局部)名称时,也可以中断它。如果您在操作一个object属性、list索引、dict值等,甚至+=也帮不了你,它不会看到“简单的STORE”,因此它不会清除目标引用,所有这些都会导致超流、不到位行为:

foo.x += mystr
foo[0] += mystr
foo['x'] += mystr

它还特定于str类型;在python2中,优化对unicode对象没有帮助,在python3中,它对bytes对象没有帮助,在这两个版本中,它都不会为str的子类进行优化;这些子类总是走慢路。在

基本上,对于不熟悉Python的人来说,在最简单的常见情况下,优化是尽可能好的,但是对于稍微复杂的情况,优化不会带来严重的麻烦。这只是强化了PEP8的建议:如果您可以通过正确的操作并使用str.join在每个解释器上对任何存储目标运行得更快,那么取决于解释器的实现细节是个坏主意。在

相关问题 更多 >

    热门问题