在Python中重置生成器对象

Dew*_*wfy 133 python yield generator

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

y = FunctionWithYield()
for x in y: print(x)
#here must be something to reset 'y'
for x in y: print(x)
Run Code Online (Sandbox Code Playgroud)

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

nos*_*klo 133

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

  1. 再次运行生成器函数,重新启动生成:

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

    y = list(FunctionWithYield())
    for x in y: print(x)
    # can iterate again:
    for x in y: print(x)
    
    Run Code Online (Sandbox Code Playgroud)

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

因此,您拥有经典的内存与处理权衡.我无法想象一种在没有存储值或再次计算它们的情况下重绕发生器的方法.

  • 当然这是问题的正确答案. (4认同)
  • @Dewfy:确定:def call_my_func():返回FunctionWithYield(param1,param2) (3认同)
  • (1) 的另一个缺点也是 FunctionWithYield() 不仅成本高昂,而且*不可能*重新计算,例如,如果它从标准输入读取。 (2认同)
  • 为了回应@Max 所说的,如果函数的输出可能(或将)在调用之间发生变化,(1) 可能会产生意外和/或不良结果。 (2认同)

Ant*_*sma 107

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

y = FunctionWithYield()
y, y_backup = tee(y)
for x in y:
    print(x)
for x in y_backup:
    print(x)
Run Code Online (Sandbox Code Playgroud)

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

  • 如果您想知道它在这种情况下会做什么,它实际上是缓存列表中的元素.所以你不妨使用`y = list(y)`,其余代码不变. (27认同)
  • @Dewfy:**会慢****因为无论如何都必须复制所有项目. (9认同)
  • 是的,在这种情况下list()更好.tee仅在您不使用整个列表时才有用 (7认同)
  • 查看implmentation(http://docs.python.org/library/itertools.html#itertools.tee) - 这使用延迟加载策略,因此列出的项目仅在需要时复制 (6认同)
  • tee()将在内部创建一个列表来存储数据,这与我在答案中的相同. (5认同)
  • `tee()`不是我的发球杯.为什么不将`y`转换为函数:`y = lambda:FunctionWithYield()`,然后`for y in y():` (4认同)
  • `tee` 适用于无限迭代。`list` 没有 (3认同)
  • @jeromerg这必须是Python的禅宗:`gl = lambda:((x,y)表示范围内的x(10)表示范围内(x)的y)`然后`list(gl())`与您一样多喜欢。+ 1,ser。 (2认同)

小智 31

>>> 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
Run Code Online (Sandbox Code Playgroud)

  • 它有 2 个缺点:1)直到 StopIteration 才能耗尽;2)它不能与任何生成器(例如范围)一起使用 (2认同)

Aar*_*lla 28

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

data = ExpensiveSetup()
for x in FunctionWithYield(data): pass
for x in FunctionWithYield(data): pass
Run Code Online (Sandbox Code Playgroud)

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

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


mic*_*den 19

我想为一个老问题提供不同的解决方案

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)
Run Code Online (Sandbox Code Playgroud)

相比于像这样做的好处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)
Run Code Online (Sandbox Code Playgroud)


Han*_*Gay 5

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

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


Ben*_*man 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))
Run Code Online (Sandbox Code Playgroud)

输出:

[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
Run Code Online (Sandbox Code Playgroud)


Aal*_*lok 5

使用包装函数来处理 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
Run Code Online (Sandbox Code Playgroud)

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

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

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