python list comprehension vs + =

时间:2013-05-27 21:57:10

标签: python performance list-comprehension augmented-assignment

今天我试图找到一个方法,在python中对字符串进行一些处理。比我说的更高级的程序员不使用+=但是使用''.join()我也可以在例如:http://wiki.python.org/moin/PythonSpeed/#Use_the_best_algorithms_and_fastest_tools中读到这个。 但是我自己测试了这个并且发现了一些奇怪的结果(这不是我想要再次猜测它们但是我想要坚持下去)。 我的想法是,如果有一个包含空格的字符串"This is \"an example text\"“该字符串应转换为Thisis"an example text"containingspaces空格将被删除,但仅在引号之外。

我使用''.join(list)测量了我的算法的两个不同版本的性能,使用+=测量了一个

import time

#uses '+=' operator
def strip_spaces ( s ):
    ret_val = ""
    quote_found = False
    for i in s:
        if i == '"':
            quote_found = not quote_found

        if i == ' ' and quote_found == True:
            ret_val += i

        if i != ' ':
            ret_val += i
    return ret_val

#uses "".join ()   
def strip_spaces_join ( s ):
    #ret_val = ""
    ret_val = []
    quote_found = False
    for i in s:
        if i == '"':
            quote_found = not quote_found

        if i == ' ' and quote_found == True:
            #ret_val = ''.join( (ret_val, i) )
            ret_val.append(i)

        if i != ' ':
            #ret_val = ''.join( (ret_val,i) )
            ret_val.append(i)
    return ''.join(ret_val)


def time_function ( function, data):
    time1 = time.time();
    function(data)
    time2 = time.time()
    print "it took about {0} seconds".format(time2-time1)

在我的机器上,这使用+=

为算法产生了一个小优势
print '#using += yields ', timeit.timeit('f(string)', 'from __main__ import string, strip_spaces as f', number=1000)
print '#using \'\'.join() yields ', timeit.timeit('f(string)', 'from __main__ import string, strip_spaces_join as f', number=1000)

与timeit计时时间:

#using += yields  0.0130770206451
#using ''.join() yields  0.0108470916748

差异很小。但是为什么''.join()没有明确地执行使用+=的函数,但是对于'.join()版本似乎有一个小优势。 我在Ubuntu 12.04上用python-2.7.3

测试了这个

4 个答案:

答案 0 :(得分:8)

比较算法时,请使用正确的方法;使用timeit module消除CPU利用率和交换的波动。

使用timeit显示两种方法之间几乎没有区别,但''.join() 稍微更快:

>>> s = 1000 * string
>>> timeit.timeit('f(s)', 'from __main__ import s, strip_spaces as f', number=100)
1.3209099769592285
>>> timeit.timeit('f(s)', 'from __main__ import s, strip_spaces_join as f', number=100)
1.2893600463867188
>>> s = 10000 * string
>>> timeit.timeit('f(s)', 'from __main__ import s, strip_spaces as f', number=100)
14.545105934143066
>>> timeit.timeit('f(s)', 'from __main__ import s, strip_spaces_join as f', number=100)
14.43651008605957

函数中的大部分工作是循环遍历每个字符并测试引号和空格,而不是字符串连接本身。此外,''.join()变体做了更多工作;您首先将元素附加到列表中(这将替换+=字符串连接操作),然后您使用''.join()在最后连接这些值。而且这种方法仍然有点快。

您可能想要删除正在进行的工作以比较只是连接部分:

def inplace_add_concatenation(s):
    res = ''
    for c in s:
        res += c

def str_join_concatenation(s):
    ''.join(s)

显示:

>>> s = list(1000 * string)
>>> timeit.timeit('f(s)', 'from __main__ import s, inplace_add_concatenation as f', number=1000)
6.113742113113403
>>> timeit.timeit('f(s)', 'from __main__ import s, str_join_concatenation as f', number=1000)
0.6616439819335938

这表明''.join()串联仍然是 heck +=快得多。速度差异在于循环; s在两种情况下都是列表,但是''.join()遍历C中的值,而另一个版本必须在Python中循环它。这就完全不同了。

答案 1 :(得分:3)

另一种选择是编写一个使用生成器连接的函数,而不是每次都附加到列表。

例如:

def strip_spaces_gen(s):
    quote_found = False
    for i in s:
        if i == '"':
            quote_found = not quote_found
        if i == ' ' and quote_found == True:
            # Note: you (c|sh)ould drop the == True, but I'll leave it here so as to not give an unfair advantage over the other functions
            yield i
        if i != ' ':
            yield i

def strip_spaces_join_gen(ing):
     return ''.join(strip_spaces_gen(ing))

这似乎与较短的字符串大致相同(作为连接):

In [20]: s = "This is \"an example text\" containing spaces"

In [21]: %timeit strip_spaces_join_gen(s)
10000 loops, best of 3: 22 us per loop

In [22]: %timeit strip_spaces(s)
100000 loops, best of 3: 13.8 us per loop

In [23]: %timeit strip_spaces_join(s)
10000 loops, best of 3: 23.1 us per loop

但对于较大的字符串来说速度更快。

In [24]: s = s * 1000

In [25]: %timeit strip_spaces_join_gen(s)
100 loops, best of 3: 12.9 ms per loop

In [26]: %timeit strip_spaces(s)
100 loops, best of 3: 17.1 ms per loop

In [27]: %timeit strip_spaces_join(s)
100 loops, best of 3: 17.5 ms per loop

答案 2 :(得分:3)

(这可能是OP已经知道的很多细节,但完整地解决这个问题可以帮助那些最终解决这个问题的人)

mystring += suffix中的问题是字符串是不可变的,所以这实际上等同于mystring = mystring + suffix。因此,实现必须创建一个新的字符串对象,将mystring中的所有字符复制到它,然后在suffix之后复制所有字符。然后mystring名称被反弹以引用新字符串; <{1}}引用的原始字符串对象未受影响。

就其本身而言,这实际上并不是问题。连接这两个字符串的任何方法都必须这样做,包括mystring;这实际上是更糟,因为它必须首先构造一个列表对象然后迭代它,并且在拼接空字符串时没有实际的数据传输在''.join([mystring, suffix])mystring之间,至少需要一条指令才能进行整理。

suffix成为问题的地方是重复。像这样:

+=

请记住,mystring = '' for c in 'abcdefg' * 1000000: mystring += c 相当于mystring += c。因此,在循环的第一次迭代中,它评估mystring = mystring + c复制1个字符的总数。接下来它'' + 'a'总共复制2个字符。然后'a' + 'b'代表3个字符,然后'ab' + 'c'代表4代,我想你可以看到它的发展方向。每个后续的'abc' + 'd'都会重复上一个所有的工作,然后再复制新的字符串。这非常浪费。

+=更好,因为你等到你知道要复制其中任何一个的所有字符串,然后将每个字符串直接复制到最终字符串对象中的正确位置。与一些评论和答案所说的相反,即使您必须修改循环以将字符串附加到字符串列表,然后在循环之后''.join(...)它们,这仍然是这种情况。 列表不是不可变的,因此追加到列表会修改它,并且它只需要附加单个引用而不是复制字符串中的所有字符。对列表进行数千次追加比执行数千次字符串join操作更快

重复字符串+=理论上即使没有循环也是一个问题,如果你只是编写类似的源代码:

+=

但在实践中,除非所涉及的字符串非常庞大,否则您不太可能手动编写足够长的代码序列。因此,请注意循环(或递归函数)中的s = 'foo' s += 'bar' s += 'baz' ...


当您尝试计时时,您可能看不到此结果的原因是,实际上对CPython解释器中的字符串+=进行了优化。让我们回到我的愚蠢的例子循环:

+=

每次mystring = '' for c in 'abcdefg' * 1000000: mystring += c mystring = mystring + c值变为垃圾并被删除,名称mystring最终会引用新创建的字符串它始于旧对象的内容。我们可以通过识别mystring即将成为垃圾来优化这一点,因此我们可以做任何我们喜欢的事情,而不需要任何人关心。因此,即使字符串在Python级别是不可变的,在实现级别我们也可以使它们动态扩展,并且我们通过正常分配新的字符串和复制方法来实现mystring,或者通过扩展目标字符串并仅复制源字符,具体取决于target += source是否为垃圾

这种优化的问题在于它很容易破坏。它适用于小型自包含循环(顺便说一下,它最容易转换为使用target)。但是,如果您正在做一些更复杂的事情而且您不小心最终会遇到多个字符串引用,那么代码突然运行得慢得多。

假设您在循环中有一些日志记录调用,并且日志记录系统缓冲其消息一段时间以便一次打印它们(应该是安全的;字符串是不可变的)。日志记录系统中对字符串的引用可能会阻止join优化适用。

假设您已将循环编写为递归函数(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_joinplus_equals_maybe_non_garbage进行比较,您可以看到,即使优化仅在每千次plus_equals_enumerate次操作中失败一次,但仍有5倍的性能损失

+=的优化实际上只是为了拯救那些没有经验的Python程序员,或者只是快速懒惰地编写一些临时代码的人。如果您正在考虑自己在做什么,那么您应该使用+=


摘要:对于固定少量连接,使用join是合适的。 +=总是更好地使用循环来构建字符串。实际上,由于join优化,您可能看不到将代码从+=移植到join的巨大改进。你仍然应该使用+=,因为优化是不可靠的,当它无法启动时的差异可能是巨大的。

答案 3 :(得分:1)

+=.join之间的效果差异取决于很多因素:

  1. 操作系统。在unix-like或Windows系统上为越来越大的字符串运行它可以产生完全不同的结果。通常,在Windows下,您会看到运行时间增加得多。

  2. Python实现。默认情况下,我们讨论CPython,但还有其他实现,如Jython或PyPy。我们来看看PyPy。使用上面答案中的源代码:

    CPython 2.7:
    python concat.py 
    inplace_add_concatenation: 0.420897960663
    str_join_concatenation:    0.061793088913
    ratio: 6.81140833169
    
    PyPy 1.9:
    pypy concat.py 
    inplace_add_concatenation: 1.26573014259
    str_join_concatenation:    0.0392870903015
    ratio: 32.2174570038
    
  3. 尽管与CPython相比,PyPy因其速度提升而闻名, +=版本的速度较慢。这是一个故意的决定,不包括在内 PyPy的默认版本中的`+ ='优化。

    处理表现的经验法则:“永远不要猜测,总是衡量。”

    阅读文档也有帮助:

      

    6 CPython实现细节:如果s和t都是字符串,有些   像CPython这样的Python实现通常可以就地执行   对s = s + t或s + = t形式的赋值进行优化。什么时候   适用时,此优化使二次运行时间更少   有可能。此优化既是版本又是实现   依赖。对于性能敏感的代码,最好使用   str.join()方法,确保一致的线性串联   跨版本和实现的性能。“”“

         

    来自http://docs.python.org/2/library/stdtypes.html#typesseq