Python中的迭代器可以重置吗?

169

我能否在Python中重置迭代器/生成器?我正在使用DictReader并希望将其重置为文件开头。


3
顺便提一下,我发现list()函数会遍历它的参数(一个可迭代对象)。因此,如果对相同的可迭代对象(例如zip()的结果)调用两次list(),第二次调用将返回空列表! - dz902
16个回答

99
我看到很多答案建议使用 itertools.tee,但是这忽略了它文档中的一个关键警告:
该迭代器可能需要大量辅助存储器(取决于需要存储多少临时数据)。通常情况下,如果一个迭代器在另一个迭代器开始之前使用了大部分或全部数据,则使用 `list()` 而不是 `tee()` 的速度更快。
基本上,`tee` 是为那些存在两个(或更多)克隆同一迭代器时,虽然彼此“失去同步”,但是它们并没有太多离开彼此的情况而设计的,相反,它们保持在同一“邻域”内(仅相差几个项目)。对于 OP 的“从头开始重做”的问题不太适用。
另一方面,只要字典列表可以轻松地适合内存,“`L = list(DictReader(...))`” 就非常适用。通过 `iter(L)` 可以随时创建新的“从头开始的迭代器”(非常轻巧和低开销),并且可以部分或全部地使用而不影响新的或现有的迭代器;其他访问模式也很容易使用。
正如几个答案所指出的那样,在特定情况下,对于 `csv`,您还可以对底层文件对象进行 `.seek(0)`(一个相当特殊的情况)。虽然我不确定是否已经记录和保证,但它目前确实有效。对于真正巨大的 csv 文件,考虑使用 `list` 作为一般方法可能会产生太大的内存占用。

7
使用list()函数缓存对一个5MB的csvreader进行多次遍历,可以将我的运行时间从大约12秒降低到约0.5秒。 - John Mee

40
如果你有一个名为 'blah.csv' 的 csv 文件,它看起来像这样:
a,b,c,d
1,2,3,4
2,3,4,5
3,4,5,6

你知道可以打开要读取的文件,并使用指定的方法创建一个DictReader

blah = open('blah.csv', 'r')
reader= csv.DictReader(blah)

接下来,您可以通过使用reader.next()获取下一行内容,该行应该被输出。

{'a':1,'b':2,'c':3,'d':4}

再次使用会产生

{'a':2,'b':3,'c':4,'d':5}

然而,在这个时候如果你使用 blah.seek(0),那么下一次调用 reader.next() 时会得到:

{'a':1,'b':2,'c':3,'d':4}

这似乎是你要寻找的功能。然而,我相信与这种方法相关的一些技巧我可能不知道。 @Brian 建议简单地创建另一个 DictReader。如果你的第一个 reader 正在读取文件的一半,这种方法将不起作用,因为你的新 reader 将从文件的任何位置获得意外的键和值。


这正是我的理论告诉我的,很高兴看到我认为应该发生的事情确实发生了。 - Wayne Werner
@Wilduck:如果您创建一个新的文件句柄并将其传递给第二个DictReader,那么您所描述的使用另一个DictReader的行为就不会发生,对吗? - user248237
如果您有两个文件处理程序,它们将独立运行。 - Wilduck

31

不。Python的迭代器协议非常简单,仅提供一个单一方法(.next()__next__()),一般情况下没有重置迭代器的方法。

通常的模式是再次使用相同的过程创建新的迭代器。

如果您想要“保存”一个迭代器以便可以返回到开头,则可以使用itertools.tee复制迭代器。


1
虽然你对 .next() 方法的分析可能是正确的,但有一种相当简单的方法可以得到 op 所要求的内容。 - Wilduck
2
@Wilduck:我看到你的答案了。我只回答了关于迭代器的问题,对于csv模块我一无所知。希望两个答案都对原帖发布者有用。 - u0b34a0f6ae
严格来说,迭代器协议也要求实现 __iter__ 方法。也就是说,迭代器也必须是可迭代对象。 - Steve Jessop

12

使用.seek(0)可能会出现错误,正如Alex Martelli和Wilduck所建议的那样。问题在于下一次调用.next()时,你将得到一个形式为{key1:key1, key2:key2, ...}的标题行字典。解决方法是在file.seek(0)之后调用reader.next()来删除标题行。

因此,您的代码应该类似于以下内容:

f_in = open('myfile.csv','r')
reader = csv.DictReader(f_in)

for record in reader:
    if some_condition:
        # reset reader to first row of data on 2nd line of file
        f_in.seek(0)
        reader.next()
        continue
    do_something(record)

12

是的,如果您使用numpy.nditer来构建迭代器。

>>> lst = [1,2,3,4,5]
>>> itr = numpy.nditer([lst])
>>> itr.next()
1
>>> itr.next()
2
>>> itr.finished
False
>>> itr.reset()
>>> itr.next()
1

nditer可以像itertools.cycle一样循环遍历数组吗? - LWZ
1
@LWZ:我不这么认为,但你可以尝试使用try:next(),在StopIteration异常时执行reset() - Dennis Williamson
跟着一个next() - Dennis Williamson
这就是我一直在寻找的! - sriram
1
请注意,此处“操作数”的限制为32:https://stackoverflow.com/questions/51856685/python-np-nditer-valueerror-too-many-operands - Simon

8

也许与原问题不相关,但可以将迭代器包装在一个返回迭代器的函数中。

def get_iter():
    return iterator

要重置迭代器,只需再次调用该函数即可。

如果该函数需要一些参数,则可以使用functools.partial创建一个闭包,以代替原始的迭代器。这当然是微不足道的,如果该函数不需要任何参数。

def get_iter(arg1, arg2):
   return iterator
from functools import partial
iter_clos = partial(get_iter, a1, a2)

这似乎避免了tee(n个副本)或list(1个副本)需要执行的缓存操作。

4

对于小文件,您可以考虑使用more_itertools.seekable - 这是一个第三方工具,提供可重置的可迭代对象。

演示

import csv

import more_itertools as mit


filename = "data/iris.csv"
with open(filename, "r") as f:
    reader = csv.DictReader(f)
    iterable = mit.seekable(reader)                    # 1
    print(next(iterable))                              # 2
    print(next(iterable))
    print(next(iterable))

    print("\nReset iterable\n--------------")
    iterable.seek(0)                                   # 3
    print(next(iterable))
    print(next(iterable))
    print(next(iterable))

输出

{'Sepal width': '3.5', 'Petal width': '0.2', 'Petal length': '1.4', 'Sepal length': '5.1', 'Species': 'Iris-setosa'}
{'Sepal width': '3', 'Petal width': '0.2', 'Petal length': '1.4', 'Sepal length': '4.9', 'Species': 'Iris-setosa'}
{'Sepal width': '3.2', 'Petal width': '0.2', 'Petal length': '1.3', 'Sepal length': '4.7', 'Species': 'Iris-setosa'}

Reset iterable
--------------
{'Sepal width': '3.5', 'Petal width': '0.2', 'Petal length': '1.4', 'Sepal length': '5.1', 'Species': 'Iris-setosa'}
{'Sepal width': '3', 'Petal width': '0.2', 'Petal length': '1.4', 'Sepal length': '4.9', 'Species': 'Iris-setosa'}
{'Sepal width': '3.2', 'Petal width': '0.2', 'Petal length': '1.3', 'Sepal length': '4.7', 'Species': 'Iris-setosa'}

在这里,一个DictReader对象被包装在一个可寻址对象(1)中并被提前(2)。使用seek()方法将迭代器重置/倒回到第0个位置(3)。
注意: 迭代时内存消耗增加,因此对于大文件应谨慎使用此工具,正如文档中所示

3

一个可能的选择是使用itertools.cycle(),这将允许您无限迭代而不需要像.seek(0)这样的技巧。

iterDic = itertools.cycle(csv.DictReader(open('file.csv')))

2

虽然没有迭代器重置,但是Python 2.6(及更高版本)中的“itertools”模块有一些实用工具可以帮助您。其中之一是“tee”,它可以制作迭代器的多个副本,并缓存运行在前面的一个结果,以便这些结果在副本上使用。这将满足您的需求:

>>> def printiter(n):
...   for i in xrange(n):
...     print "iterating value %d" % i
...     yield i

>>> from itertools import tee
>>> a, b = tee(printiter(5), 2)
>>> list(a)
iterating value 0
iterating value 1
iterating value 2
iterating value 3
iterating value 4
[0, 1, 2, 3, 4]
>>> list(b)
[0, 1, 2, 3, 4]

2

在'iter()'调用期间,返回一个新创建的迭代器,该迭代器位于最后一次迭代处

class ResetIter: 
  def __init__(self, num):
    self.num = num
    self.i = -1

  def __iter__(self):
    if self.i == self.num-1: # here, return the new object
      return self.__class__(self.num) 
    return self

  def __next__(self):
    if self.i == self.num-1:
      raise StopIteration

    if self.i <= self.num-1:
      self.i += 1
      return self.i


reset_iter = ResetRange(10)
for i in reset_iter:
  print(i, end=' ')
print()

for i in reset_iter:
  print(i, end=' ')
print()

for i in reset_iter:
  print(i, end=' ')

输出:

0 1 2 3 4 5 6 7 8 9 
0 1 2 3 4 5 6 7 8 9 
0 1 2 3 4 5 6 7 8 9 

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接