Restablecer el objeto generador en Python

Tengo objeto generador devuelto por rendimiento múltiple. La preparación para llamar a este generador requiere bastante tiempo de operación. Es por eso que quiero reutilizar el generador varias veces.

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

Por supuesto, estoy pensando en copiar contenido en una lista simple.

Otra opción es usar la función itertools.tee() para crear una segunda versión de su generador:

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

Esto podría ser beneficioso desde el punto de vista del uso de la memoria si la iteración original no procesa todos los elementos.

Los generadores no pueden ser rebobinados. Tienes las siguientes opciones:

  1. Ejecute de nuevo la función del generador, reiniciando la generación:

     y = FunctionWithYield() for x in y: print(x) y = FunctionWithYield() for x in y: print(x) 
  2. Almacene los resultados del generador en una estructura de datos en la memoria o en el disco que puede repetir una vez más:

     y = list(FunctionWithYield()) for x in y: print(x) # can iterate again: for x in y: print(x) 

La desventaja de la opción 1 es que vuelve a calcular los valores. Si eso requiere mucha CPU, terminas calculando dos veces. Por otro lado, la desventaja de 2 es el almacenamiento. La lista completa de valores se almacenará en la memoria. Si hay demasiados valores, eso puede ser poco práctico.

Así que tienes la memoria clásica frente a la compensación de procesamiento . No puedo imaginar una forma de rebobinar el generador sin almacenar los valores o calcularlos nuevamente.

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

Probablemente la solución más simple es envolver la parte costosa en un objeto y pasarla al generador:

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

De esta manera, puede almacenar en caché los cálculos caros.

Si puede mantener todos los resultados en la RAM al mismo tiempo, entonces use list() para materializar los resultados del generador en una lista simple y trabajar con eso.

Quiero ofrecer una solución diferente a un viejo problema.

 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) 

El beneficio de esto cuando se compara con algo parecido a la list(iterator) es que se trata de O(1) espacio de complejidad y la list(iterator) es O(n) . La desventaja es que, si solo tiene acceso al iterador, pero no a la función que lo produjo, no puede usar este método. Por ejemplo, podría parecer razonable hacer lo siguiente, pero no funcionará.

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

Si la respuesta de GrzegorzOledzki no es suficiente, probablemente podría usar send() para lograr su objective. Vea PEP-0342 para más detalles sobre generadores mejorados y expresiones de rendimiento.

ACTUALIZACIÓN: Ver también itertools.tee() . Involucra parte de la compensación de procesamiento de memoria en comparación con el proceso mencionado anteriormente, pero puede ahorrar algo de memoria en solo almacenar los resultados del generador en una list ; Depende de cómo uses el generador.

De la documentación oficial del tee :

En general, si un iterador usa la mayoría o todos los datos antes de que comience otro iterador, es más rápido usar list () en lugar de tee ().

Así que es mejor usar la list(iterable) en su caso.

Si su generador es puro en el sentido de que su salida solo depende de los argumentos pasados ​​y del número de paso, y desea que el generador resultante sea reiniciable, aquí hay un fragmento de orden que puede ser útil:

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

salidas:

 [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 

Puedes definir una función que devuelve tu generador.

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

Ahora puedes hacer tantas veces como quieras:

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

No hay opción para restablecer los iteradores. El iterador generalmente aparece cuando se itera a través de la función next() . La única manera es hacer una copia de seguridad antes de iterar en el objeto iterador. Compruebe a continuación.

Creando objetos iteradores con elementos del 0 al 9

 i=iter(range(10)) 

Iterando a través de la función next () que saldrá

 print(next(i)) 

Convertir el objeto iterador a la lista

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

así que el artículo 0 ya está salido. También se muestran todos los elementos a medida que convertimos el iterador en una lista.

 next(L) Traceback (most recent call last): File "", line 1, in  next(L) StopIteration 

Por lo tanto, debe convertir el iterador en listas para realizar una copia de seguridad antes de iniciar la iteración. La lista se puede convertir a iterador con iter()

Ahora puede usar more_itertools.seekable (una herramienta de terceros) que permite restablecer los iteradores.

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

Nota: el consumo de memoria aumenta a medida que avanza el iterador, así que desconfíe de iterables grandes.

No estoy seguro de lo que querías decir con preparación cara, pero supongo que realmente tienes

 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) 

Si ese es el caso, ¿por qué no reutilizar los data ?

Ok, dices que quieres llamar a un generador varias veces, pero la inicialización es costosa … ¿Qué pasa con algo como esto?

 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 

Alternativamente, puedes hacer tu propia clase que siga el protocolo del iterador y defina algún tipo de función de “reinicio”.

 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

Usando una función de envoltura para manejar StopIteration

Podría escribir una función de envoltura simple a su función de generación de generador que rastrea cuando el generador está agotado. Lo hará utilizando la excepción StopIteration lanza un generador cuando llega al final de la iteración.

 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 

Como puede ver arriba, cuando nuestra función de envoltura StopIteration una excepción StopIteration , simplemente reinicializa el objeto generador (usando otra instancia de la llamada de función).

Y luego, suponiendo que defina su función de suministro de generador en algún lugar como se muestra a continuación, puede usar la syntax del decorador de funciones de Python para envolverlo implícitamente:

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

Se puede hacer por código de objeto. Aquí está el ejemplo.

 code_str="y=(a for a in [1,2,3,4])" code1=compile(code_str,'','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