我很好奇消费迭代器最快的方法是什么,也是最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_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)
上获得了类似的效果,这是评论中的建议。
答案 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