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

Posted

技术标签:

【中文标题】使用迭代器的最快(最 Pythonic)方式【英文标题】:Fastest (most Pythonic) way to consume an iterator 【发布时间】:2018-11-28 23:33:54 【问题描述】:

我很好奇使用迭代器的最快方式是什么,也是最 Pythonic 的方式。

例如,假设我想创建一个带有 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]

有效,但他们都觉得笨重。有没有更 Pythonic 的方法来确保迭代器快速迭代,以便您从一些副作用中受益?


基准测试

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

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) 获得了类似的性能,这是 cmets 中的建议。

【问题讨论】:

代替[_ for _ in my_map],你也可以只做list(my_map)并丢弃结果 我很确定这是一个老问题的重复,我很确定我在自己的答案中提到了那个老问题......但我找不到它。 (我可以找到一些答案,例如 this one,但它们都明确表示 deque 是最快的,没有链接……)。 如果你想减少内存分配,你也可以使用[0 for _ in my_map if False]而不是[_ for _ in my_map]。它也应该更快。 【参考方案1】:

虽然您不应该仅仅为了副作用而创建地图对象,但实际上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。

【讨论】:

太好了,谢谢!我应该补充一点,一般来说,这是一个坏主意,但不管你信不信,我发现了一个我需要这样做的案例! @zvone:更快。添加了时间。 @Engineero 这种情况并不常见,但确实会出现——否则,itertools 不会在文档中提供它的配方,deque 不会有在 C 源代码中对其进行了优化。 值得注意的是,在 PyPy 中,deque 并未针对 maxlen=0 进行优化,因此它实际上比 for 循环一个数量级( 23.3us 对 1.17us 在我的笔记本电脑上)。【参考方案2】:

如果你只关心 CPython,deque 是最快的方法,如 user2357112's answer 所示。1 2.7 和 3.2 以及 32- vs. 64 位,以及 Windows 与 Linux,等等。

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

尤其是在 PyPy 中,源代码中没有这样的优化,2 并且 JIT 无法优化该无操作 append 输出。 (而且很难看出它为什么每次循环都不需要至少一个警卫测试。)当然与 Python 中循环的成本相比……对吧?但是 Python 中的循环在 PyPy 中非常快,几乎与 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 安装程序。

【讨论】:

这太好了,谢谢!我并没有真正考虑过不同解释器之间的实现差异。我目前被困在 Jupyter 笔记本中。我将不得不更多地研究这方面...... @Engineero 如果您还不知道:Jupyter 笔记本可以运行 PyPy 内核。 (上次我尝试使用带有 CPython 笔记本的 PyPy 内核时出现了一些故障,而相反的方法工作正常但无法安装本地 Qt 的东西。但如果你不需要同时运行两个内核,你只能有两个并行的 Jupyter 安装。)【参考方案3】:

more_itertools 包提供了一个consume() 方法。但在我的电脑(python 3.5)上,它与双端队列解决方案相当。您可以检查它是否对您的特定口译员有好处。

>>>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

【讨论】:

以上是关于使用迭代器的最快(最 Pythonic)方式的主要内容,如果未能解决你的问题,请参考以下文章

迭代器的学习心得

迭代器的学习心得

集合中Iterator迭代器的使用以及实现原理。

Python:迭代器的简单理解

迭代器的抽象

python学习之-迭代器