使用迭代器的最快(最 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_set
和my_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)方式的主要内容,如果未能解决你的问题,请参考以下文章