代码之家  ›  专栏  ›  技术社区  ›  Engineero

使用迭代器的最快(最pythonic)方式

  •  8
  • Engineero  · 技术社区  · 6 年前

    我很好奇使用迭代器的最快方式是什么,以及最蟒蛇式的方式是什么。

    例如,假设我想创建一个迭代器 map 作为副作用积累的内建物。我其实不在乎 地图 ,只是副作用,所以我想用尽可能少的开销或模板来完成迭代。类似于:

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

    在这个例子中,我只想遍历迭代器来积累 my_set MyoSET 只是一个空的集合 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))
    

    具有 MyoSET My-映射 如上所述。我及时得到了以下结果:

    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) ,这是评论中的一个建议。

    2 回复  |  直到 6 年前
        1
  •  12
  •   user2357112    6 年前

    虽然不应该仅仅为了副作用而创建map对象,但实际上有一个标准的方法可以使用 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 在使用DEA的DEQE实现中 麦克伦= 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实现中,解释程序开销的整个性质是非常不同的,并且 麦克伦= 0 快速路径是cpython特有的。见 abarnert's answer 对于其他Python实现。

        2
  •  8
  •   abarnert    6 年前

    如果你只关心cpython, deque 是最快的方法,如 user2357112's answer . 同样的事情已经在2.7和3.2中被证明了,32对64,Windows和Linux,等等。

    但是这依赖于CPython的C实现中的优化。 德克 . 其他实现可能没有这样的优化,这意味着它们最终调用了 append 对于每个元素。

    特别是在PyPy,在源头上没有这样的优化, jit无法优化 追加 出来。(很难理解为什么每次循环都不需要至少一个保护测试)当然,与python中循环的成本相比,是吗?但是python中的循环在pypy中非常快,几乎和cpython中的c循环一样快,所以这实际上是一个巨大的区别。

    比较时间(在用户回答中使用相同的测试:

              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
    

    这当然给了我127美元在CPython,和1.41US在PyPy。


    1。当然,您可以编写一个自定义的C扩展,但它只会以一个很小的常量更快——您可以在跳转到快速路径之前避免构造函数调用和测试,但一旦进入该循环,您就必须准确地执行它正在执行的操作。

    2。通过pypy源代码进行跟踪总是很有趣的,但我认为它最终会在 W_Deque 类,这是内置的一部分 _collections 模块。

    三.cpython 3.6.4;pypy 5.10.1/3.5.3;都来自各自的标准64位macos安装程序。