在Python中重置生成器对象

时间:2009-08-13 11:10:48

标签: python generator yield

我有多个yield返回的生成器对象。准备调用这台发电机是相当费时的操作。这就是我想多次重复使用生成器的原因。

y = FunctionWithYield()
for x in y: print(x)
#here must be something to reset 'y'
for x in y: print(x)

当然,我正在考虑将内容复制到简单列表中。

18 个答案:

答案 0 :(得分:127)

发电机不能倒带。您有以下选择:

  1. 再次运行生成器功能,重新启动生成:

    y = FunctionWithYield()
    for x in y: print(x)
    y = FunctionWithYield()
    for x in y: print(x)
    
  2. 将生成器结果存储在内存或磁盘上的数据结构中,您可以重复迭代:

    y = list(FunctionWithYield())
    for x in y: print(x)
    # can iterate again:
    for x in y: print(x)
    
  3. 选项 1 的缺点是它会再次计算值。如果那是CPU密集型的,你最终会计算两次。另一方面, 2 的缺点是存储。整个值列表将存储在内存中。如果值太多,那可能是不切实际的。

    所以你拥有经典的内存与处理权衡。我无法想象一种在不存储值或再次计算它们的情况下重绕发生器的方法。

答案 1 :(得分:101)

另一种选择是使用itertools.tee()函数创建生成器的第二个版本:

y = FunctionWithYield()
y, y_backup = tee(y)
for x in y:
    print(x)
for x in y_backup:
    print(x)

如果原始迭代可能不处理所有项目,那么从内存使用的角度来看,这可能是有益的。

答案 2 :(得分:29)

>>> def gen():
...     def init():
...         return 0
...     i = init()
...     while True:
...         val = (yield i)
...         if val=='restart':
...             i = init()
...         else:
...             i += 1

>>> g = gen()
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> g.next()
3
>>> g.send('restart')
0
>>> g.next()
1
>>> g.next()
2

答案 3 :(得分:27)

最简单的解决方案可能是将昂贵的部分包装在一个对象中并将其传递给生成器:

data = ExpensiveSetup()
for x in FunctionWithYield(data): pass
for x in FunctionWithYield(data): pass

这样,您可以缓存昂贵的计算。

如果你可以同时将所有结果保存在RAM中,那么使用list()在普通列表中实现生成器的结果并使用它。

答案 4 :(得分:14)

我想为旧问题提供不同的解决方案

class IterableAdapter:
    def __init__(self, iterator_factory):
        self.iterator_factory = iterator_factory

    def __iter__(self):
        return self.iterator_factory()

squares = IterableAdapter(lambda: (x * x for x in range(5)))

for x in squares: print(x)
for x in squares: print(x)

list(iterator)相比,这样做的好处是,O(1)空间复杂度为list(iterator)O(n)g = (x * x for x in range(5)) squares = IterableAdapter(lambda: g) for x in squares: print(x) for x in squares: print(x) 。缺点是,如果您只能访问迭代器,而不能访问生成迭代器的函数,那么您就无法使用此方法。例如,执行以下操作似乎是合理的,但它不起作用。

with_items

答案 5 :(得分:5)

如果GrzegorzOledzki的回答不够,你可以使用send()来完成你的目标。有关增强型生成器和yield表达式的更多详细信息,请参阅PEP-0342

更新:另见itertools.tee()。它涉及上面提到的一些内存与处理权衡,但可能保存一些内存而不仅仅是将生成器结果存储在list;这取决于你如何使用发电机。

答案 6 :(得分:4)

如果你的生成器在某种意义上是纯粹的,它的输出只依赖于传递的参数和步骤编号,并且你希望生成的生成器可以重启,这里有一个可能很方便的排序片段:

import copy

def generator(i):
    yield from range(i)

g = generator(10)
print(list(g))
print(list(g))

class GeneratorRestartHandler(object):
    def __init__(self, gen_func, argv, kwargv):
        self.gen_func = gen_func
        self.argv = copy.copy(argv)
        self.kwargv = copy.copy(kwargv)
        self.local_copy = iter(self)

    def __iter__(self):
        return self.gen_func(*self.argv, **self.kwargv)

    def __next__(self):
        return next(self.local_copy)

def restartable(g_func: callable) -> callable:
    def tmp(*argv, **kwargv):
        return GeneratorRestartHandler(g_func, argv, kwargv)

    return tmp

@restartable
def generator2(i):
    yield from range(i)

g = generator2(10)
print(next(g))
print(list(g))
print(list(g))
print(next(g))

输出:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
0
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1

答案 7 :(得分:3)

来自official documentation of tee

  

通常,如果一个迭代器使用之前的大部分或全部数据   另一个迭代器启动,使用list()而不是tee()更快。

因此,最好在您的情况下使用list(iterable)

答案 8 :(得分:2)

您可以定义一个返回您的生成器的函数

def f():
  def FunctionWithYield(generator_args):
    code here...

  return FunctionWithYield

现在你可以随心所欲地做多次:

for x in f()(generator_args): print(x)
for x in f()(generator_args): print(x)

答案 9 :(得分:2)

使用包装函数处理StopIteration

您可以在生成器的函数中编写一个简单的包装函数,以跟踪生成器何时耗尽。它将使用生成器到达迭代结束时抛出的StopIteration异常来实现。

import types

def generator_wrapper(function=None, **kwargs):
    assert function is not None, "Please supply a function"
    def inner_func(function=function, **kwargs):
        generator = function(**kwargs)
        assert isinstance(generator, types.GeneratorType), "Invalid function"
        try:
            yield next(generator)
        except StopIteration:
            generator = function(**kwargs)
            yield next(generator)
    return inner_func

正如您在上面看到的那样,当我们的包装器函数捕获到StopIteration异常时,它只是简单地重新初始化了生成器对象(使用函数调用的另一个实例)。

然后,假设您在如下所示的位置定义了生成器提供的函数,则可以使用Python函数装饰器语法隐式包装它:

@generator_wrapper
def generator_generating_function(**kwargs):
    for item in ["a value", "another value"]
        yield item

答案 10 :(得分:1)

我不确定你的昂贵准备是什么意思,但我猜你确实有

data = ... # Expensive computation
y = FunctionWithYield(data)
for x in y: print(x)
#here must be something to reset 'y'
# this is expensive - data = ... # Expensive computation
# y = FunctionWithYield(data)
for x in y: print(x)

如果是这种情况,为什么不重用data

答案 11 :(得分:1)

没有重置迭代器的选项。迭代器通常在迭代next()函数时弹出。唯一的方法是在迭代迭代器对象之前进行备份。检查下面。

使用项目0到9

创建迭代器对象
i=iter(range(10))

迭代将弹出的next()函数

print(next(i))

将迭代器对象转换为列表

L=list(i)
print(L)
output: [1, 2, 3, 4, 5, 6, 7, 8, 9]

所以第0项已经弹出。当我们将迭代器转换为列表时,也会弹出所有项目。

next(L) 

Traceback (most recent call last):
  File "<pyshell#129>", line 1, in <module>
    next(L)
StopIteration

因此,您需要在开始迭代之前将迭代器转换为备份列表。 可以使用iter(<list-object>)

将列表转换为迭代器

答案 12 :(得分:1)

您现在可以使用more_itertools.seekable(第三方工具)来重置迭代器。

通过> pip install more_itertools

安装
import more_itertools as mit


y = mit.seekable(FunctionWithYield())
for x in y:
    print(x)

y.seek(0)                                              # reset iterator
for x in y:
    print(x)

注意:在推进迭代器的过程中,内存消耗会增加,所以请注意大型迭代。

答案 13 :(得分:1)

您可以使用itertools.cycle() 您可以使用此方法创建一个迭代器,然后在该迭代器上执行for循环,该循环将遍历其值。

例如:

def generator():
for j in cycle([i for i in range(5)]):
    yield j

gen = generator()
for i in range(20):
    print(next(gen))

将生成20个数字,重复0到4。

文档中的注释:

Note, this member of the toolkit may require significant auxiliary storage (depending on the length of the iterable).

答案 14 :(得分:0)

好的,你说你想多次调用一个生成器,但初始化很昂贵......这样的事情怎么样?

class InitializedFunctionWithYield(object):
    def __init__(self):
        # do expensive initialization
        self.start = 5

    def __call__(self, *args, **kwargs):
        # do cheap iteration
        for i in xrange(5):
            yield self.start + i

y = InitializedFunctionWithYield()

for x in y():
    print x

for x in y():
    print x

或者,您可以创建自己的类,该类遵循迭代器协议并定义某种“重置”函数。

class MyIterator(object):
    def __init__(self):
        self.reset()

    def reset(self):
        self.i = 5

    def __iter__(self):
        return self

    def next(self):
        i = self.i
        if i > 0:
            self.i -= 1
            return i
        else:
            raise StopIteration()

my_iterator = MyIterator()

for x in my_iterator:
    print x

print 'resetting...'
my_iterator.reset()

for x in my_iterator:
    print x

https://docs.python.org/2/library/stdtypes.html#iterator-types http://anandology.com/python-practice-book/iterators.html

答案 15 :(得分:0)

我的答案解决了一个稍微不同的问题:如果生成器的初始化成本很高,而每个生成的对象的生成成本却很高。但是我们需要在多个函数中多次使用生成器。为了精确地调用生成器和每个生成的对象一次,我们可以使用线程,并在不同的线程中运行每个使用方法。由于GIL,我们可能无法实现真正​​的并行性,但是我们将实现我们的目标。

这种方法在以下情况下表现出色:深度学习模型处理了大量图像。结果是图像上许多对象的大量蒙版。每个掩码都会占用内存。我们大约有10种方法可以进行不同的统计和度量,但是它们会同时拍摄所有图像。所有图像都无法容纳在内存中。这些方法可以轻松地重写为接受迭代器。

class GeneratorSplitter:
'''
Split a generator object into multiple generators which will be sincronised. Each call to each of the sub generators will cause only one call in the input generator. This way multiple methods on threads can iterate the input generator , and the generator will cycled only once.
'''

def __init__(self, gen):
    self.gen = gen
    self.consumers: List[GeneratorSplitter.InnerGen] = []
    self.thread: threading.Thread = None
    self.value = None
    self.finished = False
    self.exception = None

def GetConsumer(self):
    # Returns a generator object. 
    cons = self.InnerGen(self)
    self.consumers.append(cons)
    return cons

def _Work(self):
    try:
        for d in self.gen:
            for cons in self.consumers:
                cons.consumed.wait()
                cons.consumed.clear()

            self.value = d

            for cons in self.consumers:
                cons.readyToRead.set()

        for cons in self.consumers:
            cons.consumed.wait()

        self.finished = True

        for cons in self.consumers:
            cons.readyToRead.set()
    except Exception as ex:
        self.exception = ex
        for cons in self.consumers:
            cons.readyToRead.set()

def Start(self):
    self.thread = threading.Thread(target=self._Work)
    self.thread.start()

class InnerGen:
    def __init__(self, parent: "GeneratorSplitter"):
        self.parent: "GeneratorSplitter" = parent
        self.readyToRead: threading.Event = threading.Event()
        self.consumed: threading.Event = threading.Event()
        self.consumed.set()

    def __iter__(self):
        return self

    def __next__(self):
        self.readyToRead.wait()
        self.readyToRead.clear()
        if self.parent.finished:
            raise StopIteration()
        if self.parent.exception:
            raise self.parent.exception
        val = self.parent.value
        self.consumed.set()
        return val

用法:

genSplitter = GeneratorSplitter(expensiveGenerator)

metrics={}
executor = ThreadPoolExecutor(max_workers=3)
f1 = executor.submit(mean,genSplitter.GetConsumer())
f2 = executor.submit(max,genSplitter.GetConsumer())
f3 = executor.submit(someFancyMetric,genSplitter.GetConsumer())
genSplitter.Start()

metrics.update(f1.result())
metrics.update(f2.result())
metrics.update(f3.result())

答案 16 :(得分:0)

它对我如何工作。

csv_rows = my_generator()
for _ in range(10):
    for row in csv_rows:
        print(row)
    csv_rows = my_generator()

答案 17 :(得分:-3)

可以通过代码对象完成。这是一个例子。

code_str="y=(a for a in [1,2,3,4])"
code1=compile(code_str,'<string>','single')
exec(code1)
for i in y: print i

1 2 3 4

for i in y: print i


exec(code1)
for i in y: print i

1 2 3 4