如何在一行代码中高效地遍历迭代器并将其耗尽?

23

如果我有一个迭代器 it 并且想要耗尽它,我可以这样写:

for x in it:
    pass

有没有内置或标准库函数可以让我用一行代码来实现?当然我也可以这样做:

list(it)

该代码将使用迭代器构建列表,然后丢弃它。但是,由于列表构建过程,我认为这是低效的。当然,编写一个辅助函数来执行空for循环是微不足道的,但我想知道是否还有其他方法。


我不确定你想要什么,但是[None for _ in it]这个代码行可以完成任务(或者类似的变体)吗? - Mathias711
假设您不是为了副作用而这样做,那么耗尽迭代器与仅丢弃它相比有何好处? - snakecharmerb
纯粹出于好奇 - 为什么? - nigel222
2
这确实是为了副作用而存在的。任何建立列表的解决方案都比下面的解决方案“collections.deque(it, maxlen=0)”更低效。 - hpk42
相关链接:[https://dev59.com/FFUL5IYBdhLWcg3wDUW2](最快(最Pythonic)消耗迭代器的方法) - asynts
6个回答

26

来自itertools的示例:

    # feed the entire iterator into a zero-length deque
    collections.deque(iterator, maxlen=0)

1
也可以在这里简要解释: http://code.activestate.com/lists/python-ideas/23364/ - Reblochon Masque
3
谢谢 - 我已经尝试了谷歌和stackoverflow搜索,但没有找到解决方案。现在应该更容易发现了 :) 而且collections.exhaust_iteratoritertools.exhaust_iterator会更好,更明显。 - hpk42

6

2022年更新(悬赏问答):标准库中没有“专用功能”,deque(it, 0)仍然是最有效的。这就是为什么在itertools的consume配方more-itertools的consume函数中使用它(点击[源代码]).

各种提案的基准测试,每个元素的迭代时间,遍历itertools.repeat(None, 10 ** 5) (使用CPython 3.10):

  2.7 ns ± 0.1 ns consume_deque
  6.5 ns ± 0.0 ns consume_loop
  6.5 ns ± 0.0 ns consume_all_if_False
 13.9 ns ± 0.3 ns consume_object_in
 27.0 ns ± 0.1 ns consume_all_True
 29.4 ns ± 0.3 ns consume_sum_0
 44.8 ns ± 0.1 ns consume_reduce
deque 由于其是C语言编写,且具有最大长度为0时的快速路径,因此获胜。该快速路径不与元素进行任何操作
简单循环方法以Python迭代方式的速度获得第二名。之前在此处提出的其他解决方案通过对每个元素执行更多或更少的工作来浪费更多或更少的时间。我添加了consume_all_if_False,以显示如何有效地执行all/sum:增加一个if False子句,以便你的生成器不产生任何内容。

基准测试代码 (在线测试!):

def consume_loop(it):
    for _ in it:
        pass

def consume_deque(it):
    deque(it, 0)

def consume_object_in(it):
    object() in it

def consume_all_True(it):
    all(True for _ in it)

def consume_all_if_False(it):
    all(_ for _ in it if False)

def consume_sum_0(it):
    sum(0 for _ in it)

def consume_reduce(it):
    reduce(lambda x, y: y, it)

funcs = [
    consume_loop,
    consume_deque,
    consume_object_in,
    consume_all_True,
    consume_all_if_False,
    consume_sum_0,
    consume_reduce,
]

from timeit import default_timer as timer
from itertools import repeat
from collections import deque
from functools import reduce
from random import shuffle
from statistics import mean, stdev

times = {f: [] for f in funcs}
def stats(f):
    ts = [t * 1e9 for t in sorted(times[f])[:5]]
    return f'{mean(ts):5.1f} ns ± {stdev(ts):3.1f} ns'

for _ in range(25):
  shuffle(funcs)
  for f in funcs:
    n = 10**5
    it = repeat(None, n)
    t0 = timer()
    f(it)
    t1 = timer()
    times[f].append((t1 - t0) / n)

for f in sorted(funcs, key=stats):
  print(stats(f), f.__name__)

作为“特殊情况的快速路径”的补充,CPython专门编写了一个内部函数来消耗迭代器,在deque.extend(和deque.extendleft)中调用该函数,当maxlen为0时。 - Mechanic Pig
@MechanicPig 好的,最终还是加上了链接,谢谢。昨晚有点懒。 - Kelly Bundy
有趣的是,它是在这里添加的(https://github.com/python/cpython/commit/060c7f6bbafdaeb4b73ce34f1bb34e4ac76f2d92)。另一个竞争解决方案是使用`itertools.islice`,例如`for _ in islice(it, None, None, 1_000_000): pass,如果他们决定删除deque`的快速路径,这对我来说似乎是公平的。 - Simply Beautiful Art

3
请注意,您的建议也可以被简化为一行代码:
for _ in it: pass

我刚刚制作了:

def exhaust(it):
    for _ in it:
        pass

与使用 deque 的解决方案相比(在我的笔记本上比较慢,慢了10%),但我认为这种方法更简洁。


0

object() in it

如果您知道迭代器永远不会产生某种类型的对象,您也可以使用该对象,例如None in it() in it。新创建的object()几乎在任何情况下都有效,因为它永远不会等于其他任何东西(除非有什么诡计)。

我并不是在“倡导”这个惯用法;问题中的for循环在许多方面都是最佳解决方案。但是,如果您正在寻找一种令人毛骨悚然的“优雅”答案,即在尽可能少的副作用计算的情况下仍然是一个非常整洁的单行代码(而不是例如any(False for _ in it)),那么这可能就是它。


-1

内置的all()函数应该非常便宜和简单:

all(True for _ in it)

编辑:已修复,谢谢 @hemflit!


1
不会的,这会在第一个 falsy 元素上停止迭代。但是 all(True for _ in it) 可以实现。 - hemflit

-1
你可以使用 sum:
sum(0 for _ in it)

或者类似地,使用reduce

reduce(lambda x, y: y, it)

根据我的基准测试,这两个是最慢的,真的没有“高效”(尽管我使用了更新的Python进行测量,但我相信它们在以前就已经效率低下了)。 - Kelly Bundy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接