Restablecer objeto generador en Python


153

Tengo un objeto generador devuelto por rendimiento múltiple. La preparación para llamar a este generador es una operación bastante lenta. 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 teniendo en cuenta copiar el contenido en una lista simple. ¿Hay alguna manera de restablecer mi generador?

Respuestas:


119

Otra opción es usar la itertools.tee()función 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 memoria si la iteración original no procesara todos los elementos.


33
Si se pregunta qué hará en este caso, se trata esencialmente de elementos de caché en la lista. Por lo tanto, podría usarlo y = list(y)con el resto de su código sin cambios.
ilya n.

55
tee () creará una lista internamente para almacenar los datos, por lo que es lo mismo que hice en mi respuesta.
nosklo

66
Mira la implicación ( docs.python.org/library/itertools.html#itertools.tee ): esto utiliza una estrategia de carga diferida, por lo que los elementos para enumerar se copian solo a pedido
Dewfy

11
@Dewfy: que será más lento ya que todos los elementos tendrán que copiarse de todos modos.
nosklo

8
sí, list () es mejor en este caso. tee solo es útil si no consume toda la lista
gravitación

148

Los generadores no se pueden rebobinar. Tienes las siguientes opciones:

  1. Ejecute la función de generador nuevamente, 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 nuevamente:

    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.

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


¿Puede existir una forma de guardar la firma de la llamada de función? FunctionWithYield, param1, param2 ...
Dewfy

3
@Dewfy: seguro: def call_my_func (): return FunctionWithYield (param1, param2)
nosklo

@Dewfy ¿Qué quiere decir con "guardar firma de llamada de función"? ¿Podría explicar por favor? ¿Te refieres a guardar los parámetros pasados ​​al generador?
Андрей Беньковский

2
Otra desventaja de (1) también es que FunctionWithYield () puede ser no solo costoso, sino imposible de volver a calcular, por ejemplo, si está leyendo desde stdin.
Max

2
Para hacer eco de lo que dijo @Max, si la salida de la función puede (o cambiará) entre llamadas, (1) puede dar resultados inesperados y / o indeseables.
Sam_Butler

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

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 costosos cálculos.

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


23

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 así list(iterator)es que esto es O(1)complejidad espacial y lo list(iterator)es O(n). La desventaja es que, si solo tiene acceso al iterador, pero no a la función que produjo el iterador, entonces no puede usar este método. Por ejemplo, puede 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)

@Dewfy En el primer fragmento, el generador está en la línea "cuadrados = ...". Las expresiones generadoras se comportan de la misma manera que llamar a una función que usa rendimiento, y yo solo usé una porque es menos detallada que escribir una función con rendimiento para un ejemplo tan breve. En el segundo fragmento, he usado FunctionWithYield como generator_factory, por lo que se llamará cada vez que se llame iter , que es cuando escribo "for x in y".
michaelsnowden

Buena solución. En realidad, esto hace que un objeto iterable sin estado sea un objeto iterador con estado, por lo que el objeto en sí es reutilizable. Especialmente útil si desea pasar un objeto iterable a una función y esa función usará el objeto varias veces.
Cosyn

5

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

ACTUALIZACIÓN: Ver también itertools.tee(). Involucra parte de ese intercambio de memoria versus procesamiento mencionado anteriormente, pero puede ahorrar algo de memoria en lugar de almacenar los resultados del generador en a list; depende de cómo estés usando el generador.


5

Si su generador es puro en el sentido de que su salida solo depende de argumentos pasados ​​y el número de paso, y desea que el generador resultante sea reiniciable, aquí hay un fragmento de clasificación que podría 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

3

De la documentación oficial de tee :

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

Por lo tanto, es mejor usarlo list(iterable)en su caso.


66
¿Qué pasa con los generadores infinitos?
Dewfy

1
La velocidad no es la única consideración; list()pone todo el iterable en memoria
Chris_Rands

@Chris_Rands También lo hará tee()si un iterador consume todos los valores, así es como teefunciona.
AChampion

2
@Dewfy: para generadores infinitos, use la solución de Aaron Digulla (función ExpensiveSetup que devuelve los datos preciosos).
Jeff Learman

3

Usar una función de contenedor para manejar StopIteration

Podría escribir una función envoltura simple en su función generadora de generador que rastrea cuando el generador está agotado. Lo hará utilizando la StopIterationexcepción que arroja 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 contenedor detecta una StopIterationexcepción, simplemente reinicia el objeto generador (utilizando otra instancia de la llamada a la función).

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

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

2

Puede definir una función que devuelva su 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)

1
Gracias por la respuesta, pero el punto principal de la pregunta era evitar la creación , invocar la función interna solo oculta la creación: la creas dos veces
Dewfy

1

No estoy seguro de qué querías decir con preparación costosa, 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 data?


1

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

Crear objeto iterador con elementos del 0 al 9

i=iter(range(10))

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

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]

entonces el elemento 0 ya está desplegado. Además, todos los elementos aparecen cuando convertimos el iterador a la lista.

next(L) 

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

Por lo tanto, debe convertir el iterador en listas para la copia de seguridad antes de comenzar a iterar. La lista podría convertirse a iterador coniter(<list-object>)


1

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

Instalar a través de > 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 crece mientras avanza el iterador, así que tenga cuidado con los iterables grandes.


1

Puede hacerlo utilizando itertools.cycle () , puede crear un iterador con este método y luego ejecutar un bucle for sobre el iterador que recorrerá sus valores.

Por ejemplo:

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

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

generará 20 números, de 0 a 4 repetidamente.

Una nota de los documentos:

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

+1 porque funciona, pero veo 2 problemas allí 1) gran huella de memoria ya que la documentación dice "crear una copia" 2) El bucle infinito definitivamente no es lo que quiero
Dewfy

0

Ok, dices que quieres llamar a un generador varias veces, pero la inicialización es costosa ... ¿Qué tal 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, puede crear su propia clase que siga el protocolo iterador y defina algún tipo de función de 'restablecimiento'.

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


Simplemente delega el problema al contenedor. Suponga que la costosa inicialización crea un generador. Mi pregunta era acerca de cómo restablecer su interior__call__
Dewfy

Se agregó un segundo ejemplo en respuesta a su comentario. Esto es esencialmente un generador personalizado con un método de reinicio.
tvt173

0

Mi respuesta resuelve un problema ligeramente diferente: si el generador es costoso de inicializar y cada objeto generado es costoso de generar. Pero necesitamos consumir el generador varias veces en múltiples funciones. Para llamar al generador y a cada objeto generado exactamente una vez, podemos usar subprocesos y ejecutar cada uno de los métodos de consumo en diferentes subprocesos. Es posible que no logremos un verdadero paralelismo debido a GIL, pero lograremos nuestro objetivo.

Este enfoque hizo un buen trabajo en el siguiente caso: el modelo de aprendizaje profundo procesa muchas imágenes. El resultado es una gran cantidad de máscaras para muchos objetos en la imagen. Cada máscara consume memoria. Tenemos alrededor de 10 métodos que hacen diferentes estadísticas y métricas, pero toman todas las imágenes a la vez. Todas las imágenes no pueden caber en la memoria. Los métodos pueden reescribirse fácilmente para aceptar el iterador.

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

Uso:

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

Simplemente reinventa itertools.isliceo para asíncrono aiostream.stream.take, y esta publicación le permite hacerlo de forma asinosa / espera stackoverflow.com/a/42379188/149818
Dewfy

-3

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

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


44
bueno, en realidad era necesario reiniciar el generador para evitar la doble ejecución del código de inicialización. Su enfoque (1) ejecuta la inicialización dos veces de todos modos, (2) implica execque es un poco no recomendado para un caso tan simple.
Rocío
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.