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

93 投票
6 回答
40171 浏览
提问于 2025-04-18 17:28

考虑以下代码:

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 loop 几次,因为 data 不是空的。但是,它并不会打印 second loop为什么第一次遍历 data 有用,但第二次就不行了?我该怎么才能让它第二次也能用呢?

除了 for 循环,任何类型的遍历都会出现同样的问题:列表/集合/字典推导式,把迭代器传给 list()sum() 或者 reduce() 等等。

另一方面,如果 data 是另一种 可迭代对象,比如 list 或者 range(它们都是 序列),那么两个循环都会正常运行:

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

* 更多例子:


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

检测 输入是迭代器还是“可重用”的可迭代对象,请参见 确保一个参数可以被迭代两次.

6 个回答

3

为什么迭代器第二次迭代不工作?

它是“有效的”,因为示例中的 for 循环确实会运行。只是它没有进行任何迭代。 这是因为迭代器已经“耗尽”了;它已经遍历了所有的元素。

为什么其他类型的可迭代对象可以正常工作?

因为在后台,每次循环时都会为该可迭代对象创建一个新的迭代器。从头开始创建迭代器意味着它会从开始的位置开始。

这发生是因为迭代需要一个可迭代对象。如果已经提供了一个可迭代对象,它将被直接使用;否则,就需要进行转换,这样会创建一个新的对象。

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

可以通过缓存数据;从新迭代器开始(假设我们可以重新创建初始条件);或者,如果迭代器是专门设计的,可以寻址或重置迭代器。相对来说,能够寻址或重置的迭代器比较少。

缓存

唯一的通用方法是记住第一次看到的元素(或确定将要看到的元素),然后再次迭代这些元素。最简单的方法是从迭代器创建一个 listtuple

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

for element in elements:
    ...

由于 list 是一个非迭代器的可迭代对象,每次循环都会创建一个新的可迭代对象,遍历所有元素。如果在执行此操作时迭代器已经“进行了一部分”迭代,那么 list 只会包含“后续”元素:

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 函数创建的打开的 file 对象。这里的状态是磁盘上底层文件中的一个位置;.tell.seek 方法允许我们检查和修改该位置值 - 例如,.seek(0) 将位置设置到文件的开头,从而有效地重置迭代器。类似地,csv.reader 是一个文件的包装器;在该文件中寻址将影响后续的迭代结果。

在除了最简单、故意设计的情况下,重置迭代器将是困难甚至不可能的。即使迭代器被设计为可寻址,这也留下了一个问题,即弄清楚要寻址到哪里 - 也就是说,想要在迭代的特定点时,内部状态是什么。在上面展示的 powers_of 生成器的情况下,这很简单:只需修改 i。对于文件,我们需要知道在所需行的开始时,文件位置是什么,而不仅仅是行号。这就是为什么文件接口提供 .tell.seek 的原因。

以下是一个重新设计的 powers_of 示例,表示一个无界序列,并设计为通过 exponent 属性可寻址、可重置:

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 会反复请求迭代器的“下一个元素”,直到迭代器引发 StopException。这个逻辑可以通过内置的 next 函数获得。

通常,当 iter 接受一个已经是迭代器的单个参数时,会返回同一个对象而不做任何更改。但如果是其他类型的可迭代对象,则会创建一个新的迭代器对象。这直接导致了原问题中的问题。用户定义的类型可以打破这两个规则,但它们可能不应该这样做。

迭代器协议

Python 大致定义了一个“迭代器协议”,指定它如何判断一个类型是否是可迭代的(或特定的迭代器),以及类型如何提供迭代功能。细节在这些年中略有变化,但现代设置大致如下:

  • 任何具有 __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 异常传播。

  • 在所有这些机制到位的情况下,可以通过 while 实现 for 循环。具体来说,像这样的循环

for element in iterable:
    ...

大致会转化为:

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

只不过迭代器实际上并没有被赋予任何名称(这里的语法是为了强调 iter 只被调用一次,即使 ... 代码没有进行任何迭代时也会被调用)。

11

如何对一个迭代器进行两次循环?

通常这是不可能的。(稍后会解释。)相反,可以选择以下几种方法:

  • 将迭代器收集到一个可以多次循环的对象中。

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

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


迭代器的基本概念

通常来说,虽然不严格1

  • 可迭代对象:可以用for循环遍历的对象,表示一些数据。例子有:list(列表)、tuple(元组)、str(字符串)。
  • 迭代器:指向可迭代对象中某个元素的指针。

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

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

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

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

迭代器通常表示一个临时的数据“流”。这个数据源在迭代的过程中被消耗掉。这也解释了为什么我们不能对任意数据源进行多次循环。我们需要打开一个新的临时数据流(也就是创建一个新的迭代器)来做到这一点。

耗尽一个迭代器

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

iterable = "ABC"
iterator = iter(iterable)

for item in iterator:
    print(item)

让我们在SequenceIterator中支持这个功能,告诉for循环如何提取next项目:

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

等等。如果index超出了items的最后一个元素怎么办?我们应该为此抛出一个安全的异常:

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

因为第二次循环是从当前的iterator.index开始的,值为3,所以没有其他东西可以打印,因此iterator.__next__会抛出StopIteration异常,导致循环立即结束。


1 严格来说:

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

更多细节请查看 这里

13

一旦一个迭代器用完了,它就不会再产生任何结果了。

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

迭代器(比如通过调用 iter、生成器表达式,或者从生成器函数中使用 yield)是有状态的,只能使用一次。

这个问题在 Óscar López的回答 中有解释。不过,他建议使用 itertools.tee(data) 来替代 list(data) 是为了性能考虑,这个建议其实有点误导。

在大多数情况下,如果你想遍历整个 data 一遍,然后再遍历一遍,使用 tee 会比直接把整个迭代器放进列表再遍历两次要慢,而且还会占用更多内存。根据文档

这个迭代工具可能需要大量的辅助存储(这取决于需要存储多少临时数据)。一般来说,如果一个迭代器在另一个迭代器开始之前就使用了大部分或全部数据,使用 list() 会比 tee() 更快。

如果你只打算消费每个迭代器的前几个元素,或者你会在一个迭代器和另一个迭代器之间交替消费几个元素,那么使用 tee 可能更合适。

70

一个迭代器只能被使用一次。举个例子:

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

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

如果把这个迭代器放进一个for循环里,最后一次的StopIteration会让循环第一次就结束。如果你再试着在另一个for循环中使用同一个迭代器,立刻又会出现StopIteration,因为这个迭代器已经被用完了。

解决这个问题的简单方法是把所有的元素保存到一个列表里,这样就可以根据需要多次遍历这个列表。例如:

data = list(it)

不过,如果这个迭代器要遍历很多元素,而且这些元素大致是同时生成的,使用tee()来创建独立的迭代器会更好:

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

现在每个迭代器都可以单独遍历了:

next(it1)
# => 1
next(it1)
# => 2
next(it2)
# => 1
next(it2)
# => 2
next(it1)
# => 3
next(it2)
# => 3

撰写回答