(这可能是OP已经知道的很多细节,但是完整地回顾这个问题可以帮助其他最终回答这个问题的人)
mystring += suffix中的问题是字符串是不可变的,因此这实际上等同于mystring = mystring + suffix。因此,实现必须创建一个新的string对象,将mystring中的所有字符复制到它上面,然后再从suffix复制所有字符。然后,mystring的名称被反弹以引用新的字符串;mystring引用的原始字符串对象是不变的。在
就其本身而言,这其实不是问题。任何连接这两个字符串的方法都必须这样做,包括''.join([mystring, suffix]);这实际上是更糟的,因为它必须首先构造一个list对象,然后迭代它,虽然在拼接mystring和suffix之间的空字符串时没有实际的数据传输,但至少需要一条指令进行排序。在
当你重复这样做时,+=会成为一个问题。像这样:mystring = ''
for c in 'abcdefg' * 1000000:
mystring += c
请记住,mystring += c相当于mystring = mystring + c。因此,在循环的第一次迭代中,它计算'' + 'a'复制总共1个字符。接下来,'a' + 'b'总共复制2个字符。然后是3个字符的'ab' + 'c',然后是4个字符的'abc' + 'd',我想你可以看到这是怎么回事了。每一个后续的+=都在重复前一个工作的所有,然后也复制新的字符串。这是非常浪费的。在
''.join(...)更好,因为在那里,您要等到知道所有字符串后再复制其中任何一个,然后直接将每个字符串复制到最后一个字符串对象中的正确位置。与一些注释和答案所说的相反,即使您必须修改循环以将字符串附加到字符串列表中,然后在循环之后join它们,这种情况仍然存在。list并不是不可变的,因此将附加到一个列表会就地修改它,而且它只需要附加一个引用,而不必复制字符串中的所有字符。对一个列表执行数千个appends操作要比执行数千个string+=操作快得多。在
重复字符串+=理论上是一个问题,即使没有循环,如果您只是编写源代码,如:
^{pr2}$
但实际上,您不太可能手工编写足够长的代码序列,除非涉及的字符串非常庞大。所以只要注意循环(或递归函数)中的+=。在
当您尝试计时时,您可能看不到这个结果,原因是CPython解释器中实际上对字符串+=进行了优化。让我们回到我愚蠢的例子循环:mystring = ''
for c in 'abcdefg' * 1000000:
mystring += c
每次这样做mystring = mystring + c,mystring的old值就会变成垃圾并被删除,而名称mystring最终引用一个新创建的字符串,该字符串正好以旧对象的内容开头。我们可以通过认识到{}即将成为垃圾来优化这一点,因此我们可以在没有人关心的情况下做任何我们喜欢的事情。因此,即使字符串在Python级别是不可变的,在实现级别,我们将使它们动态扩展,并且我们将通过执行普通的allocate一个新的string和copy方法来实现target += source,或者通过扩展目标字符串并只复制源字符,,这取决于target是否会成为垃圾。在
这种优化的问题是它很容易被破坏。它在小型自包含循环(顺便说一下,使用join最容易转换成这种循环)上工作得非常好。但是,如果你在做一些更复杂的事情,并且你意外地得到了对字符串的不止一个引用,那么代码的运行速度会突然慢很多。在
假设在循环中有一些日志记录调用,而日志记录系统bu将其消息传递一段时间,以便一次打印所有消息(应该是安全的;字符串是不可变的)。在日志系统中对字符串的引用可能会阻止+=优化的适用性。在
假设您已经将循环编写为递归函数(Python实际上并不喜欢它,但仍然是这样)出于某种原因用+=构建一个字符串。外部堆栈帧仍将引用旧值。在
或者,你对字符串所做的是生成一系列对象,所以你要把它们传递给一个类;如果类直接将字符串存储在实例中,优化就消失了,但是如果类先处理它们,那么优化仍然有效。在
本质上,看起来像是一个非常基本的原语操作的性能要么是好的,要么是非常糟糕的,它取决于使用+=的代码之外的其他代码。在极端情况下,您可能需要对一个完全独立的文件(甚至可能是第三方软件包)进行更改,在您的某个模块中引入一个长期没有更改的大规模性能回归!在
另外,我的理解是,+=优化只在CPython上容易实现,因为它使用了引用计数;通过查看目标字符串的引用计数,您可以很容易地判断目标字符串何时是垃圾,而对于更复杂的垃圾收集,您只有删除引用并等到垃圾收集器运行;太晚了,无法决定如何实现+=。所以,同样的,在Python实现之间移植的非常简单的基本代码可能会突然运行得太慢,当您将它移到另一个实现时就变得不那么有用了。在
下面是一些基准测试来显示问题的严重程度:import timeit
def plus_equals(data):
s = ''
for c in data:
s += c
def simple_join(data):
s = ''.join(data)
def append_join(data):
l = []
for c in data:
l.append(c)
s = ''.join(l)
def plus_equals_non_garbage(data):
s = ''
for c in data:
dummy = s
s += c
def plus_equals_maybe_non_garbage(data):
s = ''
for i, c in enumerate(data):
if i % 1000 == 0:
dummy = s
s += c
def plus_equals_enumerate(data):
s = ''
for i, c in enumerate(data):
if i % 1000 == -1:
dummy = s
s += c
data = ['abcdefg'] * 1000000
for f in (
plus_equals,
simple_join,
append_join,
plus_equals_non_garbage,
plus_equals_maybe_non_garbage,
plus_equals_enumerate,
):
print '{:30}{:20.15f}'.format(f.__name__, timeit.timeit(
'm.{0.__name__}(m.data)'.format(f),
setup='import __main__ as m',
number=1
))
在我的系统上显示:plus_equals 0.066924095153809
simple_join 0.013648986816406
append_join 0.086287975311279
plus_equals_non_garbage 540.663727998733521
plus_equals_maybe_non_garbage 0.731688976287842
plus_equals_enumerate 0.156824111938477
当+=的优化工作时,+=的效果非常好(甚至比哑的append_join版本好一点)。我的数据表明,在某些情况下,可以通过将append+join替换为+=来优化代码,但是这样做的好处是不值得的,因为将来可能会有其他更改意外地出现井喷(如果在循环中还有其他实际的工作在进行,则很可能会非常小;如果没有,那么您应该使用simple_join版本)。在
通过比较plus_equals_maybe_non_garbage和{}可以看出,即使优化在每一千次+=操作中只失败一次,性能仍然会损失5倍。在
+=的优化实际上只是为了拯救那些没有经验的Python程序员,或者那些只是快速而懒惰地编写一些草稿代码的人。如果你在考虑你在做什么,你应该使用join。在
摘要:使用+=对于一个固定的少量的连接来说是很好的。join使用循环构建字符串总是更好。{{cd10>你可以在代码中看到巨大的改进。无论如何,您仍然应该使用join,因为优化是不可靠的,并且当优化失败时,差异可能是巨大的。在