为什么我不能对同一个迭代器进行两次迭代?如何“重置”迭代器或重用数据?
- 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
是迭代器时,例如列表迭代器或生成器表达式*,这不起作用:
>>> 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
*更多示例:
文件对象
通过显式生成器函数创建的生成器
filter
、map
和zip
对象(在 3.x 中)enumerate
对象csv.reader
sitertools
标准库中定义的各种迭代器
有关一般理论和术语的解释,请参阅什么是迭代器、可迭代和迭代?。
要检测输入是迭代器还是“可重用”的可迭代对象,请参阅确保参数可以迭代两次。
解决方案 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 循环对象。示例:
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."""
这里重要的是,迭代器通常不会在其内部存储任何实际数据。
迭代器通常模拟一个临时的数据“流”。该数据源由迭代过程使用。这很好地解释了为什么不能多次循环任意数据源。我们需要打开一个新的临时数据流(即创建一个新的迭代器)来做到这一点。
耗尽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])
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件