消耗迭代器的最快(最Pythonic)方式

时间:2018-06-19 22:51:25

标签: python python-3.x optimization iterator

我很好奇消费迭代器最快的方法是什么,也是最Python化的方法。

例如,假设我要创建一个内置map的迭代器,该迭代器会累积一些副作用。我实际上并不关心map的结果,而仅是副作用,因此我想以尽可能少的开销或样板完成迭代。像这样:

my_set = set()
my_map = map(lambda x, y: my_set.add((x, y)), my_x, my_y)

在此示例中,我只想遍历迭代器以在my_set中累积内容,而my_set只是一个空集,直到我实际运行my_map为止。像这样:

for _ in my_map:
    pass

或裸露

[_ for _ in my_map]

有效,但是他们俩都觉得笨拙。是否有其他Python方式可以确保迭代器快速迭代,以便您可以从副作用中受益?


基准

我在以下方面测试了上述两种方法:

my_x = np.random.randint(100, size=int(1e6))
my_y = np.random.randint(100, size=int(1e6))

具有上面定义的my_setmy_map。我在timeit上得到了以下结果:

for _ in my_map:
    pass
468 ms ± 20.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

[_ for _ in my_map]
476 ms ± 12.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

两者之间没有真正的区别,而且两者都显得笨拙。

请注意,我在list(my_map)上获得了类似的效果,这是评论中的建议。

3 个答案:

答案 0 :(得分:9)

虽然您不应该仅出于副作用创建地图对象,但实际上itertools docs中有一个消耗迭代器的标准方法:

def consume(iterator, n=None):
    "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 consume(iterator):
    collections.deque(iterator, maxlen=0)

以这种方式使用collections.deque避免了存储所有元素(因为maxlen=0)并以C速度进行迭代,而没有字节码解释开销。在双端队列实现中甚至有一个dedicated fast path,它使用maxlen=0双端队列消耗了迭代器。

时间:

In [1]: import collections

In [2]: x = range(1000)

In [3]: %%timeit
   ...: i = iter(x)
   ...: for _ in i:
   ...:     pass
   ...: 
16.5 µs ± 829 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [4]: %%timeit
   ...: i = iter(x)
   ...: collections.deque(i, maxlen=0)
   ...: 
12 µs ± 566 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

当然,这都是基于CPython的。在其他Python实现中,解释器开销的整体性质非常不同,并且maxlen=0快速路径特定于CPython。有关其他Python实现,请参见abarnert's answer

答案 1 :(得分:5)

如果只关心CPython,deque是最快的方法,如user2357112's answer所示。 1 2.7和3.2也演示了相同的内容,并且32位和64位,Windows和Linux,等等。

但这取决于CPython的deque的C实现的优化。其他实现可能没有这种优化,这意味着它们最终会为每个元素调用append

尤其是在PyPy中,源中没有这样的优化, 2 ,并且JIT无法优化无操作append。 (而且很难看到它在整个循环中每次都不需要至少进行一次防护测试。)当然,与Python循环的开销相比,是吧?但是,在PyPy中,Python的循环速度很快,几乎与CPython中的C循环一样快,因此这实际上产生了巨大的差异。

比较时间(使用与用户答案相同的测试: 3

          for      deque
CPython   19.7us   12.7us
PyPy       1.37us  23.3us

没有其他主要解释器的3.x版本,而且我都没有IPython,但是对Jython进行的快速测试显示了相似的效果。

因此,最快的可移植实施方式如下:

if sys.implementation.name == 'cpython':
    import collections
    def consume(it):
        return collections.deque(it, maxlen=0)
else:
    def consume(it):
        for _ in it:
            pass

这当然使我在CPython中为12.7us,在PyPy中为1.41us。


1。当然,您可以编写一个自定义的C扩展,但是只会用一个很小的常数来提高速度-您可以在跳转到快速路径之前避免进行构造函数调用和测试,但是一旦进入该循环,就必须完全按照自己的方式做。

2。通过PyPy源进行跟踪始终很有趣……但是我认为它最终会出现在W_Deque类中,该类是内置_collections模块的一部分。

3。 CPython 3.6.4; PyPy 5.10.1 / 3.5.3;都来自相应的标准64位macOS安装程序。

答案 2 :(得分:2)

more_itertools 包提供了一个 consume() 方法。但是在我的 PC(python 3.5)上,它与 deque 解决方案相当。您可以检查它是否为您的特定口译员带来优势。

>>>timeit.timeit(lambda: collections.deque(range(1,10000000),maxlen=0),number=10)
1.0916123000000084
>>>timeit.timeit(lambda: more_itertools.consume(range(1,10000000)),number=10)
1.092838400000005