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

2024-11-19 08:39:00
admin
原创
12
摘要:问题描述:考虑以下代码:def test(data): for row in data: print("first loop") for row in data: print("second loop") 当data是迭代...

问题描述:

考虑以下代码:

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是另一种可迭代对象,例如 alist或 a range(它们都是序列),则两个循环都会按预期运行:

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

*更多示例:

  • 文件对象

  • 通过显式生成器函数创建的生成器

  • filtermapzip对象(在 3.x 中)

  • enumerate对象

  • csv.readers

  • itertools标准库中定义的各种迭代器


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

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


解决方案 1:

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

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

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

当迭代器被提供给for循环时,lastStopIteration将导致循环第一次退出。尝试在另一个 for 循环中使用同一个迭代器将StopIteration立即导致 again,因为迭代器已经被使用。

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

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

解决方案 2:

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

Óscar López 的回答对此进行了解释,但是,出于性能原因,该回答建议使用itertools.tee(data)而不是 ,list(data)这是误导性的。在大多数情况下,如果你想要遍历整个data然后再遍历整个 ,tee那么比简单地将整个迭代器消耗到列表中然后对其进行两次迭代需要更多时间并使用更多内存。根据文档:

此 itertool 可能需要大量辅助存储(取决于需要存储多少临时数据)。一般来说,如果一个迭代器在另一个迭代器启动之前使用大部分或全部数据,则使用 会list()比更快tee()

tee如果您只使用每个迭代器的前几个元素,或者如果您交替使用一个迭代器的几个元素和另一个迭代器的几个元素,则可能是首选。

解决方案 3:

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

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

解决方案 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:

  • Iterable:表示数据的可 for 循环对象。示例: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)

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 从技术角度来看:

  • Iterable:__iter__当调用它时返回迭代器的对象。

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

更多详情请点击这里。

解决方案 5:

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

它确实“起作用”,即for示例中的循环确实运行了。它只是执行了零次迭代。发生这种情况是因为迭代器已“耗尽”;它已经迭代了所有元素。

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

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

发生这种情况是因为迭代需要可迭代对象。如果已经提供了可迭代对象,则将按原样使用;否则,需要进行转换,从而创建一个新对象。

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

通过缓存数据;使用新的迭代器重新开始(假设我们可以重新创建初始条件);或者,如果迭代器是专门为其设计的,则查找或重置迭代器。很少有迭代器提供查找或重置功能。

缓存

唯一完全通用的方法是记住第一次看到的元素(或确定将看到哪些元素)并再次迭代它们。最简单的方法是从list`tuple`迭代器创建一个或:**

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 中没有禁止这样做,但在许多情况下,提供一个简单的接口是不可行的;在大多数其他情况下,即使它可能很简单,它也不会得到支持。另一方面,对于生成器函数,所讨论的内部状态相当复杂,并且可以保护自身免受修改。

可搜索迭代器的经典示例是使用内置函数创建的打开file对象。所讨论的状态是磁盘上底层文件内的位置;和方法允许我们检查和修改该位置值 - 例如将位置设置为文件的开头,从而有效地重置迭代器。类似地,是文件的包装器;因此,在该文件内的搜索将影响迭代的后续结果。open`.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

技术细节

迭代器与可迭代对象

简要回想一下:

  • “迭代”是指依次查看某个抽象的概念性值序列中的每个元素。这可以包括:

    • 使用for循环

    • 使用理解或生成器表达式

    • 解包可迭代对象,包括使用或语法调用函数*`**`

    • 从另一个可迭代对象构造list、等tuple

  • “可迭代”是指表示此类序列的对象。(Python 文档中所谓的“序列”实际上比这更具体 - 基本上它也需要是有限的和有序的。)请注意,元素不需要“存储” - 在内存、磁盘或任何其他地方;只要我们可以在迭代过程中确定它们就足够了。

  • “迭代器”是指代表迭代过程的对象;从某种意义上说,它跟踪迭代中“我们所在的位置”。

结合这些定义,可迭代对象表示可以按指定顺序检查的元素;迭代器允许我们按指定顺序检查元素。迭代器当然“表示”这些元素 - 因为我们可以通过检查它们来找出它们是什么 - 而且它们当然可以按指定顺序进行检查 - 因为这就是迭代器所实现的。因此,我们可以得出结论,迭代器是一种可迭代对象- Python 的定义也同意这一点。

迭代的工作原理

为了进行迭代,我们需要一个迭代器。在 Python 中进行迭代时,需要一个迭代器;但在正常情况下(即,除了编写不佳的用户定义代码),任何可迭代对象都是允许的。在后台,Python 会将其他可迭代对象转换为相应的迭代器;此操作的逻辑可通过内置iter函数获得。为了进行迭代,Python 会反复向迭代器询问“下一个元素”,直到迭代器产生一个StopException。此操作的逻辑可通过内置next函数获得。

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

迭代器协议

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传播。

  • 有了所有这些机制,就可以for根据 实现循环while。具体来说,如下循环

for element in iterable:
    ...

大致翻译为:

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

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

解决方案 6:

其他答案都正确,但还有一个选项没有明确说明。这可能有点不靠谱,但有些情况需要靠谱的解决方案。

假设你被赋予了这样的一个函数,但是不允许你修改它:

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

此函数对items参数进行多次迭代,因此items只能是一定大小的集合(例如列表、元组或集合)才能实现所需的结果,否则迭代器将在调用后耗尽list。因此,如果不重写该函数,为循环提供自定义迭代器for(例如每次迭代时前进的进度条)似乎是不可能的。

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

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%|█████████████████████████████████|

(旁白:tqdm在这种情况下,将看到第一次迭代从其自己的构造函数开始,直到循环的第一次迭代结束,这可能比循环迭代要长得多。理想情况下,您希望延迟进度条的初始化,直到该生成器实际next被执行,但这是一个tqdm特定的细节。一种方法是将构造函数更改StaggeredChain__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)
[]

解决方案 7:

一个有效的解决方案是仅在需要时(当它是一个迭代器时)将可迭代对象保存到列表中。

# Python 3.8+

from collections.abc import Iterator
from typing import Iterable, TypeVar, Union, overload
from typing_extensions import assert_type

T = TypeVar("T")
IterableT = TypeVar("IterableT", bound=Iterable)

@overload
def read_iterator(iterable: Iterator[T]) -> list[T]: ...

@overload
def read_iterator(iterable: IterableT) -> IterableT: ...

def read_iterator(iterable: Iterable[T]) -> Union[list[T], Iterable[T]]:
    """
    If the iterable is an Iterator, read it to a List.
    If it's not, return it without any change.

    An Iterator can only be consumed once. When you need to iterate over the
    iterator more than once, a solution is to save all elements to a List.
    """
    if isinstance(iterable, Iterator):
        return list(iterable)
    else:
        return iterable

用法

def run(iterable: Iterable):
    reusable_iterable = read_iterator(iterable)
    for row in reusable_iterable:
        print("first loop")
    for row in reusable_iterable:
        print("second loop")

测试

def test_read_iterator():
    # An iterator can only be consumed once
    iterator_a = range(3).__iter__()
    assert list(iterator_a) == [0, 1, 2]
    assert list(iterator_a) == []

    # 'read_iterator' converts the iterator to a list, which can be consumed multiple times
    iterator_b = range(3).__iter__()
    list_b = read_iterator(iterator_b)
    assert list_b == [0, 1, 2]
    assert list(list_b) == [0, 1, 2]
    assert list(list_b) == [0, 1, 2]
    assert list(iterator_b) == []  # (the iterator is consumed now)

    # If the iterable is not an Iterator, 'read_iterator' should return it without any change
    a_range, a_list, a_tuple, a_set, a_dict = range(3), [1, 2, 3], (1, 2, 3), {1, 2, 3}, {"a": 1}
    assert a_range is read_iterator(a_range)
    assert a_list is read_iterator(a_list)
    assert a_tuple is read_iterator(a_tuple)
    assert a_set is read_iterator(a_set)
    assert a_dict is read_iterator(a_dict)

    # Tests done by Mypy
    assert_type(read_iterator(iterator_b), list[int])
    assert_type(read_iterator(a_range), range)
    assert_type(read_iterator(a_list), list[int])
    assert_type(read_iterator(a_tuple), tuple[int, int, int])
    assert_type(read_iterator(a_set), set[int])
    assert_type(read_iterator(a_dict), dict[str, int])
相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用