为什么python itertools“消耗”食谱比调用下n次更快?

时间:2013-05-18 23:04:43

标签: python python-2.7 itertools

在itertools的python文档中,它为推进迭代器n步骤提供了以下“配方”:

def consume(iterator, n):
    "Advance the iterator n-steps ahead. If n is none, consume entirely."
    # Use functions that consume iterators at C speed.
    if n is None:
        # feed the entire iterator into a zero-length deque
        collections.deque(iterator, maxlen=0)
    else:
        # advance to the empty slice starting at position n
        next(islice(iterator, n, n), None)

我想知道为什么这个配方与这样的东西根本不同(除了处理使用整个迭代器之外):

def other_consume(iterable, n):
    for i in xrange(n):
        next(iterable, None)

我使用timeit确认,正如预期的那样,上述方法要慢得多。配方中有什么可以实现这种卓越的性能?我知道它使用islice,但是看islice,它看起来与上面的代码基本相同:

def islice(iterable, *args):
    s = slice(*args)
    it = iter(xrange(s.start or 0, s.stop or sys.maxint, s.step or 1))
    nexti = next(it)
    ### it seems as if this loop yields from the iterable n times via enumerate
    ### how is this different from calling next n times?
    for i, element in enumerate(iterable): 
        if i == nexti:
            yield element
            nexti = next(it)

注意:即使不是从islice导入itertools而是使用上面显示的文档中的python等价来定义它,配方仍然更快..

编辑:timeit代码:

timeit.timeit('a = iter([random() for i in xrange(1000000)]); consume(a, 1000000)', setup="from __main__ import consume,random", number=10)
timeit.timeit('a = iter([random() for i in xrange(1000000)]); other_consume(a, 1000000)', setup="from __main__ import other_consume,random", number=10)
每次运行

时,

other_consume慢约2.5倍

2 个答案:

答案 0 :(得分:5)

配方更快的原因是它的关键部分(islicedeque)是用C实现的,而不是用纯Python实现的。部分原因是C循环比for i in xrange(n)快。另一部分是Python函数调用(例如next())比它们的C等价物更昂贵。

您从文档中复制的itertools.islice版本不正确,其性能显然很好,因为使用它的使用功能不会消耗任何东西。 (出于这个原因,我没有在下面显示该版本的测试结果,尽管它非常快!:)

以下是几种不同的实现,因此我们可以测试最快的内容:

import collections
from itertools import islice

# this is the official recipe
def consume_itertools(iterator, n):
    "Advance the iterator n-steps ahead. If n is none, consume entirely."
    # Use functions that consume iterators at C speed.
    if n is None:
        # feed the entire iterator into a zero-length deque
        collections.deque(iterator, maxlen=0)
    else:
        # advance to the empty slice starting at position n
        next(islice(iterator, n, n), None)

# your initial version, using a for loop on a range
def consume_qwwqwwq(iterator, n):
    for i in xrange(n):
        next(iterator, None)

# a slightly better version, that only has a single loop:
def consume_blckknght(iterator, n):
    if n <= 0:
        return
    for i, v in enumerate(iterator, start=1):
        if i == n:
            break

我系统上的计时(Windows 7上64位的Python 2.7.3):

>>> test = 'consume(iter(xrange(100000)), 1000)'
>>> timeit.timeit(test, 'from consume import consume_itertools as consume')
7.623556181657534
>>> timeit.timeit(test, 'from consume import consume_qwwqwwq as consume')
106.8907442334584
>>> timeit.timeit(test, 'from consume import consume_blckknght as consume')
56.81081856366518

我的评估是,几乎空的Python循环运行时间比C中的等效循环长七到八倍。一次循环两个序列(consume_qwwqwwq通过iterator调用next除for上的xrange循环外,费用大致翻了一倍。

答案 1 :(得分:0)

itertools.islice()上的文档存在缺陷,无法正确处理start == stop的边框。正是consume()使用的边缘区域。

对于islice(it, n, n)n消耗了it个元素,但不会产生任何结果。而是在StopIteration个元素被消耗之后引发n

另一方面,您用来测试的Python版本立即引发StopIteration ,而不会从it消耗任何。这使得对这个纯python版本的任何计时都不正确并且速度太快。

这是因为xrange(n, n, 1)迭代器会立即引发StopIteration

>>> it = iter(xrange(1, 1))
>>> print next(it)
Traceback (most recent call last):
  File "prog.py", line 4, in <module>
    print next(it)
StopIteration