为什么我不能对同一个迭代器进行两次迭代?如何“重置”迭代器或重用数据?

Why can't I iterate twice over the same iterator? How can I "reset" the iterator or reuse the data?

提问人:JSchwartz 提问时间:8/16/2014 最后编辑:Mateen UlhaqJSchwartz 更新时间:8/4/2023 访问量:38020

问:

考虑以下代码:

def test(data):
    for row in data:
        print("first loop")
    for row in data:
        print("second loop")

当迭代器(例如列表迭代器或生成器表达式*)时,这不起作用:data

>>> test(iter([1, 2]))
first loop
first loop
>>> test((_ for _ in [1, 2]))
first loop
first loop

这打印了几次,因为是非空的。但是,它不会打印 .为什么迭代数据第一次有效,而第二次却不行?我怎样才能让它第二次工作?first loopdatasecond loop

除了循环之外,任何类型的迭代似乎都会发生同样的问题:list/set/dict 推导式、将迭代器传递给 或 等。forlist()sum()reduce()

另一方面,如果是另一种可迭代对象,例如 a 或 a(它们都是序列),则两个循环都按预期运行:datalistrange

>>> test([1, 2])
first loop
first loop
second loop
second loop
>>> test(range(2))
first loop
first loop
second loop
second loop

* 更多示例:


有关一般理论和术语的解释,请参阅什么是迭代器、可迭代和迭代?

若要检测输入是迭代器还是“可重用”可迭代对象,请参阅确保参数可以迭代两次

Python 迭代器

评论

12赞 Ignacio Vazquez-Abrams 8/16/2014
可迭代与迭代器。
0赞 Nick Meyer 8/16/2014
我并不是说这是一个重复的,但你可能还想参考 stackoverflow.com/questions/9884132/......以获得更多的上下文/解释
0赞 Aran-Fey 6/14/2018
相关新闻: 重置迭代器对象
3赞 NoDataDumpNoContribution 2/13/2022
此问题中提供的代码不是重现问题的最短代码。这个问题可以通过提供更好的代码示例来改进。
0赞 Mateen Ulhaq 2/15/2022
@Trilarion 是的,我认为可以安全地删除,因为没有其他答案讨论代码的那部分。def _view(self,dbName): db = self.dictDatabases[dbName] data = db[3]

答:

68赞 Óscar López 8/16/2014 #1

一个迭代器只能使用一次。例如:

lst = [1, 2, 3]
it = iter(lst)

next(it)
# => 1
next(it)
# => 2
next(it)
# => 3
next(it)
# => StopIteration

当迭代器被提供给循环时,最后一个将导致它第一次退出。尝试在另一个 for 循环中使用相同的迭代器将立即导致再次使用,因为迭代器已被消耗。forStopIterationStopIteration

解决此问题的一种简单方法是将所有元素保存到一个列表中,可以根据需要多次遍历该列表。例如:

data = list(data)

但是,如果迭代器会遍历许多元素,那么使用 tee() 创建独立的迭代器会更好:

import itertools
it1, it2 = itertools.tee(data, 2) # create as many as needed

现在,每个参数都可以依次迭代:

for e in it1:
    print("first loop")

for e in it2:
    print("second loop")

评论

26赞 svk 8/17/2014
@ÓscarLópez 文档中的注释:“此迭代工具可能需要大量的辅助存储(取决于需要存储多少临时数据)。一般来说,如果一个迭代器在另一个迭代器启动之前使用了大部分或全部数据,那么使用 list() 而不是 tee() 会更快。因此,如果你正在使用,并且像你在示例中一样,你可能不会从中获得任何真正的好处(同时可能会占用一些额外的开销)。teeit1it2tee
14赞 shitpoet 9/8/2020
我支持 @svk - 在这种情况下,将以比单次调用效率稍低的方式创建迭代器值的完整副本。当可迭代中有很多元素时,不应该使用 - 这无关紧要,但当有局部使用时 - 在这种情况下,的缓存可以小于整个列表。例如,如果两个迭代器并驾齐驱,就像在呼叫中一样。teelistteeteezip(a, islice(b, 1))
13赞 cigien 2/13/2022
@user2357112supportsMonica 您对这个答案的编辑正在 meta 上讨论。
13赞 falsetru 8/16/2014 #2

一旦迭代器耗尽,它就不会再产生任何结果。

>>> it = iter([3, 1, 2])
>>> for x in it: print(x)
...
3
1
2
>>> for x in it: print(x)
...
>>>

评论

4赞 JSchwartz 8/16/2014
这是有道理的,但我该如何绕过它呢?
0赞 falsetru 8/16/2014
@JSchwartz,将迭代器转换为序列对象(,)。然后迭代序列对象。(仅当 csv 的大小不是很大时)listtuple
4赞 falsetru 8/16/2014
@JSchwartz,或者,如果可以访问基础文件对象,并且是可搜索的。您可以在第二个循环之前更改文件位置:csv_file_object.seek(0)
0赞 Karl Knechtel 1/7/2023
这个答案被我试图将问题改进为规范问题而过时了(在尽可能清楚地解释问题并给出具体例子之后,答案现在重复了问题中存在的信息)。对不起。
34赞 6 revs, 3 users 67%kaya3 #3

迭代器(例如,来自调用、生成器表达式或生成器函数)是有状态的,只能使用一次。iteryield

奥斯卡·洛佩斯(ÓscarLópez)的回答对此进行了解释,但是,该答案建议使用而不是出于性能原因具有误导性。 在大多数情况下,想要遍历整个迭代器,然后再次遍历整个迭代器,比简单地将整个迭代器消耗到一个列表中然后迭代两次需要更多的时间和更多的内存。根据文档:itertools.tee(data)list(data)datatee

此迭代工具可能需要大量的辅助存储(取决于需要存储多少临时数据)。通常,如果一个迭代器在另一个迭代器启动之前使用大部分或全部数据,则使用它而不是 .list()tee()

tee如果只使用每个迭代器的前几个元素,或者将从一个迭代器中消耗几个元素,然后使用另一个迭代器中的一些元素,则可能是首选。

评论

0赞 Karl Knechtel 1/9/2023
与创建辅助列表相比,通过一些具体的分析结果和/或对需要做的工作进行理论检查,这将更有说服力。tee
0赞 kaya3 1/9/2023
@KarlKnechtel 此声明来自文档 - 我已经编辑以包含引用和链接。我同意一些实证分析也是一种改进。
10赞 Mateen Ulhaq 2/15/2022 #4

如何循环迭代器两次?

这通常是不可能的。(稍后解释。相反,请执行下列操作之一:

  • 将迭代器收集到可以多次循环的内容中。

    items = list(iterator)
    
    for item in items:
        ...
    

    缺点:这会消耗内存。

  • 创建一个新的迭代器。制作一个新的迭代器通常只需要一微秒。

    for item in create_iterator():
        ...
    
    for item in create_iterator():
        ...
    

    缺点:迭代本身可能很昂贵(例如,从磁盘或网络读取)。

  • 重置“迭代器”。例如,使用文件迭代器:

    with open(...) as f:
        for item in f:
            ...
    
        f.seek(0)
    
        for item in f:
            ...
    

    缺点:大多数迭代器无法“重置”。


安的理念Iterator

通常,虽然不是技术上的1

  • 可迭代:一个表示数据的 for-loopable 对象。例子:。listtuplestr
  • 迭 代:指向可迭代对象的某个元素的指针。

如果我们要定义一个序列迭代器,它可能看起来像这样:

class SequenceIterator:
    index: int
    items: Sequence  # Sequences can be randomly indexed via items[index].

    def __next__(self):
        """Increment index, and return the latest item."""

这里重要的是,迭代器通常不会在自身内部存储任何实际数据。

迭代器通常对临时数据“流”进行建模。该数据源由迭代过程使用。这是一个很好的提示,说明为什么不能多次遍历任意数据源。我们需要打开一个新的临时数据流(即创建一个新的迭代器)来做到这一点。

疲惫不堪Iterator

当我们从迭代器中提取项目时,从迭代器的当前元素开始,一直持续到它完全耗尽,会发生什么?这就是循环的作用:for

iterable = "ABC"
iterator = iter(iterable)

for item in iterator:
    print(item)

让我们通过告诉循环如何提取项目来支持此功能:SequenceIteratorfornext

class SequenceIterator:
    def __next__(self):
        item = self.items[self.index]
        self.index += 1
        return item

坚持。如果超出了最后一个元素怎么办?我们应该为此提出一个安全的例外:indexitems

class SequenceIterator:
    def __next__(self):
        try:
            item = self.items[self.index]
        except IndexError:
            raise StopIteration  # Safely says, "no more items in iterator!"
        self.index += 1
        return item

现在,for 循环知道何时停止从迭代器中提取项目。

如果我们现在再次尝试遍历迭代器,会发生什么?

iterable = "ABC"
iterator = iter(iterable)

# iterator.index == 0

for item in iterator:
    print(item)

# iterator.index == 3

for item in iterator:
    print(item)

# iterator.index == 3

由于第二个循环从电流 3 开始,因此它没有其他任何内容可打印,因此引发异常,导致循环立即结束。iterator.indexiterator.__next__StopIteration


1 从技术上讲:

  • 可迭代:一个对象,在调用迭代器时返回迭代器。__iter__
  • 迭 代:可以在循环中重复调用以提取项的对象。此外,调用它应该返回它。__next____iter__self

更多细节在这里

评论

0赞 Karl Knechtel 1/9/2023
这里有很多很好的信息,但也有一些小的技术不准确之处。我开始尝试编辑它,但最终决定通过重新开始可以做得更好,材料的组织方式完全不同。
3赞 Karl Knechtel 1/9/2023 #5

为什么迭代器第二次不行?

它确实“有效”,从某种意义上说,示例中的循环确实运行。它只是执行零迭代。发生这种情况是因为迭代器已“耗尽”;它已经遍历了所有元素。for

为什么它适用于其他类型的可迭代对象?

因为,在幕后,会根据该可迭代对象为每个循环创建一个新的迭代器。从头开始创建迭代器意味着它从头开始。

发生这种情况是因为迭代需要可迭代。如果已经提供了可迭代对象,它将按原样使用;但除此之外,转换是必要的,这会创建一个新对象。

给定一个迭代器,我们如何对数据进行两次迭代?

通过缓存数据;从一个新的迭代器重新开始(假设我们可以重新创建初始条件);或者,如果迭代器是专门为它设计的,则查找或重置迭代器。相对较少的迭代器提供查找或重置。

缓存

唯一完全通用的方法是记住第一次看到的元素(或确定将看到哪些元素),然后再次迭代它们。最简单的方法是从迭代器创建一个列表元组

elements = list(iterator)
for element in elements:
    ...

for element in elements:
    ...

由于 是一个非迭代器可迭代对象,因此每个循环都将创建一个新的可迭代对象,该可迭代对象遍历所有元素。如果迭代器在执行此操作时已经“完成”迭代,则仅包含“以下”元素:listlist

abstract = (x for x in range(10)) # represents integers from 0 to 9 inclusive
next(abstract) # skips the 0
concrete = list(abstract) # makes a list with the rest
for element in concrete:
    print(element) # starts at 1, because the list does

for element in concrete:
    print(element) # also starts at 1, because a new iterator is created

更复杂的方法是使用 itertools.tee。这实质上是在迭代元素时从原始源代码创建元素的“缓冲区”,然后创建并返回几个自定义迭代器,这些迭代器通过记住索引、尽可能从缓冲区获取并在必要时追加到缓冲区(使用原始可迭代对象)来工作。(在现代 Python 版本的参考实现中,这不使用本机 Python 代码。

from itertools import tee
concrete = list(range(10)) # `tee` works on any iterable, iterator or not
x, y = tee(concrete, 2) # the second argument is the number of instances.
for element in x:
    print(element)
    if element == 3:
        break

for element in y:
    print(element) # starts over at 0, taking 0, 1, 2, 3 from a buffer

重新开始

如果我们知道并且可以在迭代开始时重新创建迭代器的起始条件,那也解决了问题。这隐含地发生在对列表进行多次迭代时:“迭代器的起始条件”只是列表的内容,从中创建的所有迭代器都给出相同的结果。再举一个例子,如果生成器函数不依赖于外部状态,我们可以简单地使用相同的参数再次调用它:

def powers_of(base, *range_args):
    for i in range(*range_args):
        yield base ** i

exhaustible = powers_of(2, 1, 12):

for value in exhaustible:
    print(value)

print('exhausted')

for value in exhaustible: # no results from here
    print(value)

# Want the same values again? Then use the same generator again:
print('replenished')
for value in powers_of(2, 1, 12):
    print(value)

可查找或可重置的迭代器

某些特定的迭代器可以使迭代“重置”到开头,甚至“搜索”到迭代中的特定点。通常,迭代器需要具有某种内部状态,以便跟踪它们在迭代中的“位置”。使迭代器“可搜索”或“可重置”仅意味着允许外部访问分别修改或重新初始化该状态。

Python 中没有任何内容禁止这样做,但在许多情况下,提供一个简单的接口是不可行的;在大多数其他情况下,即使它可能微不足道,它也不受支持。另一方面,对于生成器函数,所讨论的内部状态非常复杂,并且可以保护自身免受修改。

可搜索迭代器的经典示例是使用内置 open 函数创建的打开文件对象。有问题的状态是磁盘上基础文件中的位置;和方法允许我们检查和修改该位置值 - 例如 将位置设置为文件的开头,从而有效地重置迭代器。同样,csv.reader 是文件的包装器;因此,在该文件中查找将影响迭代的后续结果。.tell.seek.seek(0)

除了最简单的、经过深思熟虑设计的情况外,倒带迭代器将很难甚至不可能。即使迭代器被设计为可搜索的,这就留下了一个问题,即弄清楚在哪里寻找 - 即,在迭代中所需点的内部状态是什么。对于上面所示的生成器,这很简单:只需修改 .对于文件,我们需要知道所需行开头的文件位置,而不仅仅是行号。这就是为什么文件接口提供 以及 .powers_ofi.tell.seek

下面是一个重新设计的示例,它表示一个未绑定的序列,并被设计为可通过属性进行搜索、倒带和重置:powers_ofexponent

class PowersOf:
    def __init__(self, base):
        self._exponent = 0
        self._base = base
    def __iter__(self):
        return self
    def __next__(self):
        result = self._base ** self._exponent
        self._exponent += 1
        return result
    @property
    def exponent(self):
        return self._exponent
    @exponent.setter
    def exponent(self, value):
        if not isinstance(new_value, int):
            raise TypeError("must set with an integer")
        if new_value < 0:
            raise ValueError("can't set to negative value")
        self._exponent = new_value

例子:

pot = PowersOf(2)
for i in pot:
    if i > 1000:
        break
    print(i)

pot.exponent = 5 # jump to this point in the (unbounded) sequence
print(next(pot)) # 32
print(next(pot)) # 64

技术细节

迭代器与可迭代对象

回想一下,简要地说:

  • “迭代”意味着依次查看每个元素,即一些抽象的、概念性的值序列。这可能包括:
  • “可迭代”是指表示此类序列的对象。(Python 文档中所说的“序列”实际上比这更具体——基本上它也需要是有限的和有序的。请注意,这些元素不需要“存储”——在内存、磁盘或其他任何地方;我们可以在迭代过程中确定它们就足够了。
  • “迭代器”是指表示迭代过程的对象;从某种意义上说,它在迭代中跟踪“我们在哪里”。

结合定义,可迭代对象是表示可以按指定顺序检查的元素的东西;迭代器允许我们按指定顺序检查元素。当然,迭代器“表示”了这些元素——因为我们可以通过检查它们来找出它们是什么——当然,它们可以按指定的顺序进行检查——因为这是迭代器所启用的。因此,我们可以得出结论,迭代器是一种可迭代的——Python 的定义是一致的。

迭代的工作原理

为了迭代,我们需要一个迭代器。当我们在 Python 中迭代时,需要一个迭代器;但在正常情况下(即,除了写得不好的用户定义代码),任何可迭代都是允许的。在幕后,Python 会将其他可迭代对象转换为相应的迭代器;其逻辑可通过内置的 ITER 函数获得。为了迭代,Python 反复要求迭代器提供“下一个元素”,直到迭代器引发 .此逻辑可通过内置的 next 函数获得。StopException

通常,当给定一个已经是迭代器的参数时,将返回相同的对象。但是,如果它是其他类型的可迭代对象,则将创建一个新的迭代器对象。这直接导致了 OP 中的问题。 用户定义的类型可能会破坏这两个规则,但它们可能不应该。iter

迭代器协议

Python 粗略地定义了一个“迭代器协议”,该协议指定了它如何决定类型是否是可迭代的(或者具体地说是迭代器),以及类型如何提供迭代功能。多年来,细节略有变化,但现代设置的工作方式如下:

  • 任何具有 or 方法的东西都是可迭代的。任何定义方法方法的东西都是一个特定的迭代器。(特别注意,如果有 a 和 a 但没有,则 没有特定的含义,并且该对象是不可迭代的可迭代对象。__iter____getitem____iter____next____getitem____next____iter____next__

  • 给定单个参数,将尝试调用该参数的方法,验证结果是否具有方法,并返回该结果。它不能确保结果上存在方法。此类对象通常可以在需要迭代器的地方使用,但如果出现以下情况,则会失败。 被召唤到他们身上。如果没有,它将查找 ,并使用它来创建内置迭代器类型的实例。该迭代器大致等同于iter__iter____next____iter__iter__iter____getitem__

class Iterator:
    def __init__(self, bound_getitem):
        self._index = 0
        self._bound_getitem = bound_getitem
    def __iter__(self):
        return self
    def __next__(self):
        try:
            result = self._bound_getitem(self._index)
        except IndexError:
            raise StopIteration
        self._index += 1
        return result
  • 给定单个参数,将尝试调用该参数的方法,允许任何参数传播。next__next__StopIteration

  • 有了所有这些机制,就可以在 .具体来说,像forwhile

for element in iterable:
    ...

将大致转换为:

iterator = iter(iterable)
while True:
    try:
        element = next(iterator)
    except StopIteration:
        break
    ...

除了迭代器实际上没有被分配任何名称(这里的语法是强调只调用一次,即使没有代码迭代也会被调用)。iter...

评论

0赞 Karl Knechtel 1/9/2023
我最终给出的细节比我计划的要多得多,但重点都在前面。
1赞 Seb 4/25/2023 #6

其他答案都是正确的,但还有一个选项没有明确说明。它可能只是有点骇人听闻,但有些情况需要一个骇人听闻的解决方案。

假设你被赋予了这样的功能,你不允许修改:

def do_something(items):
    items_copy = list(items)
    
    for item in items:
        ...  # actual work

此函数多次遍历参数,因此只能使用大小的集合(例如列表、元组或集合)来获得所需的结果,否则迭代器将在调用 后耗尽。因此,在不重写函数的情况下,向循环提供自定义迭代器(例如在每次迭代时前进的进度条)似乎是不可能的。itemsitemslistfor

或者是吗?让我们创建一个简单的自定义迭代器,它包装了多个迭代器,并一个接一个地返回它们:

class StaggeredChain:
    def __init__(self, *iters):
        self.iters = iter(iters)
    
    def __iter__(self):
        return iter(next(self.iters, ()))

请注意,这与 itertools.chain 的不同之处在于它可以多次迭代,并且在每一步中的行为都类似于相应的单个包装迭代器:

>>> chained = StaggeredChain(range(5), range(4, -1, -1))
>>> list(chained)
[0, 1, 2, 3, 4]
>>> list(chained)
[4, 3, 2, 1, 0]
>>> list(chained)
[]

通过这个类,我们可以实现在内部循环中添加进度条的目标:

>>> from tqdm import tqdm
>>> vals = range(5)
>>> do_something(StaggeredChain(vals, tqdm(vals)))
100%|█████████████████████████████████|

(旁白:在这种情况下,第一次迭代将从它自己的构造函数开始,直到循环的第一次迭代结束,这可能比循环迭代要长得多。理想情况下,您希望延迟进度条的初始化,直到该生成器实际被“编辑”,但这是一个特定于的细节。一种方法是更改 to 的构造函数并传入生成单个迭代器的单个参数。tqdmnexttqdmStaggeredChain__init__(self, iters)

如果要求只是重复一组给定的值多次,然后停止,我们可以做这样的事情:

import itertools

class StaggeredRepeat:
    def __init__(self, vals, loops=1):
        self.iters = itertools.repeat(tuple(vals), loops)
    
    def __iter__(self):
        return iter(next(self.iters, ()))

现在,您可以迭代给定集合所需的次数:

>>> rep = StaggeredRepeat(range(5), 2)
>>> list(rep)
[0, 1, 2, 3, 4]
>>> list(rep)
[0, 1, 2, 3, 4]
>>> list(rep)
[]