关于python:为什么’new_file + = line + string’比’new_file = new_file + line + string’快得多?

Why is 'new_file += line + string' so much faster than 'new_file = new_file + line + string'?

本问题已经有最佳答案,请猛点这里访问。

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

1
new_file = new_file + line + string

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

1
new_file += line + string

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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" +"
"

                else:
                        country = line.split("~")[13]
                        try:
                                row_region = regions[country]
                        except KeyError:
                                row_region ="Undetermined"
                        new_file_content += line.strip() +"~" + row_region +"
"

                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(引用解释器)对就地字符串连接进行了优化(当附加到的字符串没有其他引用时)。在执行+操作时,不能可靠地应用这种优化,只有+=(+涉及两个实时引用,即赋值目标和操作数,前者不涉及+操作,因此优化起来比较困难。好的。

但是,根据PEP 8,你不应该依赖于此:好的。

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

Ok.

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.

Ok.

基于问题编辑的更新:是的,你打破了优化。您连接了多个字符串,而不仅仅是一个,而且python的计算结果是从左到右的,所以它必须首先进行最左边的连接。因此:好的。

1
2
new_file_content += line.strip() +"~" + row_region +"
"

完全不同于:好的。

1
2
new_file_content = new_file_content + line.strip() +"~" + row_region +"
"

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

1
2
new_file_content = (((new_file_content + line.strip()) +"~") + row_region) +"
"

因为在到达类型之前它实际上并不知道这些类型,所以它不能假定所有这些类型都是字符串,所以优化不会开始。好的。

如果您将代码的第二位更改为:好的。

1
2
new_file_content = new_file_content + (line.strip() +"~" + row_region +"
"
)

或者稍微慢一点,但仍然比慢代码快很多倍,因为它保持了cpython优化:好的。

1
2
3
4
5
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 +"
"

所以积累对cpython来说是显而易见的,你可以解决性能问题。但坦率地说,在执行这样的逻辑追加操作时,应该只使用+=+=的存在是有原因的,它为维护人员和解释人员提供了有用的信息。除此之外,对于dry来说,这是一个很好的实践;为什么不需要时将变量命名两次呢?好的。

当然,根据PEP8的指导方针,即使使用+=这里也是不好的形式。在大多数具有不变字符串的语言中(包括大多数非cpython python解释器),重复的字符串连接是painter算法schlemiel的一种形式,这会导致严重的性能问题。正确的解决方案是构建一个list字符串,然后一个join字符串,例如:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    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
"
)
        else:
            country = line.split("~")[13]
            try:
                    row_region = regions[country]
            except KeyError:
                    row_region ="Undetermined"
            new_file_content += (line.strip(),"~", row_region,"
"
)

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

它通常更快,甚至当cpython字符串连接选项可用时,而且在非cpython python解释器上也会可靠地更快,因为它使用可变的list有效地累积结果,然后允许''.join预计算字符串的总长度,同时分配最终字符串(inst一路上增量大小的EAD),并且只填充一次。好的。

旁注:对于您的特定情况,您根本不应该累积或连接。您有一个输入文件和一个输出文件,可以逐行处理。每次您要附加或积累文件内容时,只需将它们写出来(我已经清理了一点代码,以满足PEP8的遵从性和其他一些小的样式改进,而我正在进行这些改进):好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
"
)
        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 +"
"
)
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进行突变优化,但它需要解释器的帮助来确定引用计数,以便知道它可以安全地使用优化;有了你的帮助T解释器有帮助,只有C扩展模块才能从优化中受益。好的。

当解释器检测到两个操作数都是python级别的str类型(在c层,在python 3中,它仍然被称为PyUnicode,这是2.x天的遗留值,不值得更改),它调用一个特殊的unicode_concatenate函数,检查下一条指令是否是三条基本STORE_*指令之一。如果是,并且目标与左操作数相同,它将清除目标引用,因此PyUnicode_Append将只看到对操作数的单个引用,从而允许它用单个引用调用str的优化代码。好的。

这意味着不仅可以通过执行好的。

1
a = a + b + c

您也可以在所讨论的变量不是顶级(全局、嵌套或本地)名称时中断它。如果你在操作一个对象属性、一个list索引、一个dict值等,即使+=对你没有帮助,它也不会看到一个"简单的STORE",所以它不会清除目标引用,所有这些都会得到超低的、不到位的行为:好的。

1
2
3
foo.x += mystr
foo[0] += mystr
foo['x'] += mystr

它还针对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实现,并且不依赖于能够触发的优化。

1
2
3
4
5
6
output = []
for ...:
    output.append(line)

new_file ="
"
.join(output)