提问人:JSchwartz 提问时间:8/16/2014 最后编辑:Mateen UlhaqJSchwartz 更新时间:8/4/2023 访问量:38020
为什么我不能对同一个迭代器进行两次迭代?如何“重置”迭代器或重用数据?
Why can't I iterate twice over the same iterator? How can I "reset" the iterator or reuse the 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
除了循环之外,任何类型的迭代似乎都会发生同样的问题:list/set/dict 推导式、将迭代器传递给 或 等。for
list()
sum()
reduce()
另一方面,如果是另一种可迭代对象,例如 a 或 a(它们都是序列),则两个循环都按预期运行: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
* 更多示例:
- 文件对象
- 从显式生成器函数创建的生成器
Filter
、Map
和ZIP
对象(在 3.x 中)枚举
对象csv.reader
sIterTools
标准库中定义的各种迭代器
有关一般理论和术语的解释,请参阅什么是迭代器、可迭代和迭代?。
若要检测输入是迭代器还是“可重用”可迭代对象,请参阅确保参数可以迭代两次。
答:
一个迭代器只能使用一次。例如:
lst = [1, 2, 3]
it = iter(lst)
next(it)
# => 1
next(it)
# => 2
next(it)
# => 3
next(it)
# => StopIteration
当迭代器被提供给循环时,最后一个将导致它第一次退出。尝试在另一个 for 循环中使用相同的迭代器将立即导致再次使用,因为迭代器已被消耗。for
StopIteration
StopIteration
解决此问题的一种简单方法是将所有元素保存到一个列表中,可以根据需要多次遍历该列表。例如:
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")
评论
tee
it1
it2
tee
tee
list
tee
tee
zip(a, islice(b, 1))
一旦迭代器耗尽,它就不会再产生任何结果。
>>> it = iter([3, 1, 2])
>>> for x in it: print(x)
...
3
1
2
>>> for x in it: print(x)
...
>>>
评论
list
tuple
csv_file_object.seek(0)
迭代器(例如,来自调用、生成器表达式或生成器函数)是有状态的,只能使用一次。iter
yield
奥斯卡·洛佩斯(ÓscarLópez)的回答对此进行了解释,但是,该答案建议使用而不是出于性能原因具有误导性。
在大多数情况下,想要遍历整个迭代器,然后再次遍历整个迭代器,比简单地将整个迭代器消耗到一个列表中然后迭代两次需要更多的时间和更多的内存。根据文档:itertools.tee(data)
list(data)
data
tee
此迭代工具可能需要大量的辅助存储(取决于需要存储多少临时数据)。通常,如果一个迭代器在另一个迭代器启动之前使用大部分或全部数据,则使用它而不是 .
list()
tee()
tee
如果只使用每个迭代器的前几个元素,或者将从一个迭代器中消耗几个元素,然后使用另一个迭代器中的一些元素,则可能是首选。
评论
tee
如何循环迭代器两次?
这通常是不可能的。(稍后解释。相反,请执行下列操作之一:
将迭代器收集到可以多次循环的内容中。
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 对象。例子:。
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
由于第二个循环从电流 3 开始,因此它没有其他任何内容可打印,因此引发异常,导致循环立即结束。iterator.index
iterator.__next__
StopIteration
1 从技术上讲:
- 可迭代:一个对象,在调用迭代器时返回迭代器。
__iter__
- 迭 代:可以在循环中重复调用以提取项的对象。此外,调用它应该返回它。
__next__
__iter__
self
更多细节在这里。
评论
为什么迭代器第二次不行?
它确实“有效”,从某种意义上说,示例中的循环确实运行。它只是执行零迭代。发生这种情况是因为迭代器已“耗尽”;它已经遍历了所有元素。for
为什么它适用于其他类型的可迭代对象?
因为,在幕后,会根据该可迭代对象为每个循环创建一个新的迭代器。从头开始创建迭代器意味着它从头开始。
发生这种情况是因为迭代需要可迭代。如果已经提供了可迭代对象,它将按原样使用;但除此之外,转换是必要的,这会创建一个新对象。
给定一个迭代器,我们如何对数据进行两次迭代?
通过缓存数据;从一个新的迭代器重新开始(假设我们可以重新创建初始条件);或者,如果迭代器是专门为它设计的,则查找或重置迭代器。相对较少的迭代器提供查找或重置。
缓存
唯一完全通用的方法是记住第一次看到的元素(或确定将看到哪些元素),然后再次迭代它们。最简单的方法是从迭代器创建一个列表
或元组
:
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
函数创建的打开文件
对象。有问题的状态是磁盘上基础文件中的位置;和方法允许我们检查和修改该位置值 - 例如 将位置设置为文件的开头,从而有效地重置迭代器。同样,csv.reader
是文件的包装器;因此,在该文件中查找将影响迭代的后续结果。.tell
.seek
.seek(0)
除了最简单的、经过深思熟虑设计的情况外,倒带迭代器将很难甚至不可能。即使迭代器被设计为可搜索的,这就留下了一个问题,即弄清楚在哪里寻找 - 即,在迭代中所需点的内部状态是什么。对于上面所示的生成器,这很简单:只需修改 .对于文件,我们需要知道所需行开头的文件位置,而不仅仅是行号。这就是为什么文件接口提供 以及 .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 反复要求迭代器提供“下一个元素”,直到迭代器引发 .此逻辑可通过内置的 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
有了所有这些机制,就可以在 .具体来说,像
for
while
for element in iterable:
...
将大致转换为:
iterator = iter(iterable)
while True:
try:
element = next(iterator)
except StopIteration:
break
...
除了迭代器实际上没有被分配任何名称(这里的语法是强调只调用一次,即使没有代码迭代也会被调用)。iter
...
评论
其他答案都是正确的,但还有一个选项没有明确说明。它可能只是有点骇人听闻,但有些情况需要一个骇人听闻的解决方案。
假设你被赋予了这样的功能,你不允许修改:
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%|█████████████████████████████████|
(旁白:在这种情况下,第一次迭代将从它自己的构造函数开始,直到循环的第一次迭代结束,这可能比循环迭代要长得多。理想情况下,您希望延迟进度条的初始化,直到该生成器实际被“编辑”,但这是一个特定于的细节。一种方法是更改 to 的构造函数并传入生成单个迭代器的单个参数。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)
[]
评论
def _view(self,dbName): db = self.dictDatabases[dbName] data = db[3]