在Python中重置生成器对象


153

我有一个由多个yield返回的生成器对象。准备调用此生成器是相当耗时的操作。这就是为什么我想多次重用生成器。

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

当然,我会考虑将内容复制到简单列表中。有没有办法重置我的发电机?

Answers:


119

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

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

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


33
如果您想知道在这种情况下它将做什么,它实际上是在缓存列表中的元素。因此,您最好y = list(y)将其余代码保持不变。
ilya n。

5
tee()将在内部创建一个列表来存储数据,所以这与我在答案中所做的相同。
nosklo

6
看一下实现(docs.python.org/library/itertools.html#itertools.tee)-这使用延迟加载策略,因此要列出的项目仅按需复制
Dewfy

11
@Dewfy:这会比较慢,因为无论如何都必须复制所有项目。
nosklo

8
是的,在这种情况下,list()更好。“开球”仅在您不使用整个列表的情况下才有用
万有引力

148

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

  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)

选项1的缺点是它会再次计算值。如果那是CPU密集型的,那么您最终将计算两次。另一方面,2的缺点是存储空间。整个值列表将存储在内存中。如果值太多,那将是不切实际的。

因此,您将获得经典的内存与处理权衡。我无法想象一种在不存储值或再次计算值的情况下倒带生成器的方法。


可能存在一种保存函数调用签名的方法吗?FunctionWithYield,param1,param2 ...
Dewfy

3
@Dewfy:确保:def call_my_func():返回FunctionWithYield(param1,param2)
nosklo

@Dewfy“保存函数调用签名”是什么意思?你能解释一下吗?您的意思是保存传递给生成器的参数吗?
АндрейБеньковский

2
(1)的另一个缺点是FunctionWithYield()不仅成本高昂,而且无法重新计算,例如,如果从stdin中读取。
最大

2
呼应@Max所说的,如果函数的输出可能(或将)在调用之间改变,则(1)可能会产生意外和/或不良的结果。
Sam_Butler '19

36
>>> 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

29

可能最简单的解决方案是将昂贵的零件包装在一个对象中,然后将其传递给生成器:

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

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

如果可以将所有结果同时保存在RAM中,请使用,list()以将生成器的结果具体化为简单列表并进行处理。


23

我想为旧问题提供其他解决方案

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)

@Dewfy在第一个代码段中,生成器在“ squares = ...”行上。生成器表达式的行为与调用使用yield的函数的方式相同,而我仅使用了一个表达式,因为对于这样一个简短的示例,它比编写具有yield的函数的详细程度低。在第二个片段中,我使用FunctionWithYield作为generator_factory,因此无论何时调用iter,即在我为“ y中的x编写”时,都将调用它。
michaelsnowden '16

好的解决方案。实际上,这将使一个无状态的可迭代对象而不是有状态的迭代器对象,因此该对象本身是可重用的。如果要将可迭代对象传递给函数,并且该函数将多次使用该对象,则特别有用。
Cosyn

5

如果GrzegorzOledzki的答案不足够,您可能会send()用来实现目标。有关增强的生成器和yield表达式的更多详细信息,请参见PEP-0342

更新:另请参见itertools.tee()。它涉及到上面提到的一些内存与处理权衡,但是它可能比仅将生成器结果存储在list;中节省一些内存。这取决于您使用发电机的方式。


5

如果您的生成器是纯粹的,从某种意义上说,它的输出仅取决于传递的参数和步骤号,并且您希望生成的生成器可重新启动,则下面的排序片段可能会很方便:

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

3

tee的官方文档中

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

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


6
无限发电机呢?
Dewfy

1
速度不是唯一的考虑因素。list()将整个迭代过程存储在内存中
Chris_Rands

@Chris_Rands tee()如果一个迭代器消耗了所有值,这也将tee起作用-这就是工作原理。
AChampion'2

2
@Dewfy:对于无限生成器,请使用亚伦·迪古拉(Aaron Digulla)的解决方案(ExpensiveSetup函数返回珍贵的数据。)
Jeff Learman

3

使用包装函数处理 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

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)

1
谢谢你的回答,但问题的主要观点是:避免创建,调用内部函数只是隐藏的创建-创建了两次
Dewfy

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


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>)


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)

注意:前进迭代器时内存消耗会增加,因此请警惕大型可迭代对象。


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).

+1是因为它可以工作,但是我在那里看到2个问题1)由于文档指出“创建副本”,因此内存占用很大2)无限循环绝对不是我想要的
Dewfy

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


您只需将问题委托给包装器即可。假设昂贵的初始化过程会产生生成器。我的问题是关于如何在您的内部重设__call__
Dewfy

添加了第二个示例以回应您的评论。这本质上是带有重置方法的自定义生成器。
tvt173

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())

您只是重新发明itertools.islice或进行异步操作aiostream.stream.take,并且本文允许您以asyn / await的方式进行操作stackoverflow.com/a/42379188/149818
Dewfy

-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


4
好吧,实际上需要重置生成器以避免两次执行初始化代码。您的方法(1)无论如何都要执行两次初始化,(2)exec对于这种简单情况,它涉及一些不推荐的初始化。
2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.