Ripristino dell'oggetto generatore in Python


153

Ho un oggetto generatore restituito da resa multipla. La preparazione per chiamare questo generatore è un'operazione piuttosto dispendiosa in termini di tempo. Ecco perché voglio riutilizzare il generatore più volte.

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

Certo, sto pensando di copiare il contenuto in un semplice elenco. C'è un modo per ripristinare il mio generatore?

Risposte:


119

Un'altra opzione è utilizzare la itertools.tee()funzione per creare una seconda versione del generatore:

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

Ciò potrebbe essere utile dal punto di vista dell'utilizzo della memoria se l'iterazione originale potrebbe non elaborare tutti gli elementi.


33
Se ti stai chiedendo cosa farà in questo caso, è essenzialmente la memorizzazione nella cache degli elementi nell'elenco. Quindi potresti anche utilizzare y = list(y)il resto del codice invariato.
ilya n.

5
tee () creerà un elenco internamente per memorizzare i dati, quindi è lo stesso che ho fatto nella mia risposta.
nosklo,

6
Guarda l'implementazione ( docs.python.org/library/itertools.html#itertools.tee ) - utilizza una strategia di caricamento lento, quindi gli elementi da elencare copiati solo su richiesta
Dewfy

11
@Dewfy: che sarà più lento poiché tutti gli articoli dovranno comunque essere copiati.
nosklo,

8
sì, list () è meglio in questo caso. tee è utile solo se non stai consumando l'intero elenco
gravitazione

148

I generatori non possono essere riavvolti. Hai le seguenti opzioni:

  1. Eseguire di nuovo la funzione generatore, riavviando la generazione:

    y = FunctionWithYield()
    for x in y: print(x)
    y = FunctionWithYield()
    for x in y: print(x)
  2. Archivia i risultati del generatore in una struttura di dati sulla memoria o sul disco su cui puoi ripetere l'iterazione:

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

Il rovescio della medaglia dell'opzione 1 è che calcola di nuovo i valori. Se si tratta di un uso intensivo della CPU, si calcola due volte. D'altra parte, il rovescio della medaglia di 2 è la memoria. L'intero elenco di valori verrà archiviato in memoria. Se ci sono troppi valori, ciò può non essere pratico.

Quindi hai il classico compromesso tra memoria e elaborazione . Non riesco a immaginare un modo per riavvolgere il generatore senza memorizzare i valori o calcolarli di nuovo.


Può esistere un modo per salvare la firma della chiamata di funzione? FunctionWithYield, param1, param2 ...
Dewfy

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

@Dewfy Cosa intendi con "salva la firma della chiamata di funzione"? Potresti spiegare per favore? Intendi salvare i parametri passati al generatore?
Андрей Беньковский, il

2
Un altro aspetto negativo di (1) è anche che FunctionWithYield () può essere non solo costoso, ma impossibile da ricalcolare, ad esempio se sta leggendo da stdin.
Max

2
Per fare eco a quanto detto da @Max, se l'output della funzione potrebbe (o cambierà) tra le chiamate, (1) potrebbe dare risultati imprevisti e / o indesiderabili.
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

Probabilmente la soluzione più semplice è avvolgere la parte costosa in un oggetto e passarla al generatore:

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

In questo modo, è possibile memorizzare nella cache i calcoli costosi.

Se riesci a conservare tutti i risultati nella RAM contemporaneamente, utilizza list()per materializzare i risultati del generatore in un semplice elenco e lavorare con quello.


23

Voglio offrire una soluzione diversa a un vecchio 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)

Il vantaggio di questo rispetto a qualcosa di simile list(iterator)è che questa è la O(1)complessità dello spazio ed list(iterator)è O(n). Lo svantaggio è che, se si ha accesso solo all'iteratore, ma non alla funzione che ha prodotto l'iteratore, non è possibile utilizzare questo metodo. Ad esempio, potrebbe sembrare ragionevole fare quanto segue, ma non funzionerà.

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 Nel primo frammento, il generatore si trova sulla riga "quadrati = ...". Le espressioni del generatore si comportano allo stesso modo della chiamata di una funzione che utilizza la resa, e ne ho usata solo una perché è meno dettagliata della scrittura di una funzione con resa per un esempio così breve. Nel secondo frammento, ho usato FunctionWithYield come generator_factory, quindi verrà chiamato ogni volta che viene chiamato iter , ovvero ogni volta che scrivo "per x in y".
michaelsnowden,

Buona soluzione Questo in realtà rende un oggetto iterabile stateless invece di un oggetto iteratore con stato, quindi l'oggetto stesso è riutilizzabile. Particolarmente utile se si desidera passare un oggetto iterabile a una funzione e tale funzione utilizzerà l'oggetto più volte.
Cosyn,

5

Se la risposta di GrzegorzOledzki non sarà sufficiente, potresti probabilmente usare send()per raggiungere il tuo obiettivo. Vedere PEP-0342 per maggiori dettagli sui generatori avanzati e sulle espressioni di resa.

AGGIORNAMENTO: vedi anche itertools.tee(). Implica parte di quella memoria rispetto al compromesso di elaborazione sopra menzionato, ma potrebbe risparmiare un po 'di memoria semplicemente archiviando i risultati del generatore in un list; dipende da come stai usando il generatore.


5

Se il tuo generatore è puro, nel senso che il suo output dipende solo dagli argomenti passati e dal numero di passaggio e desideri che il generatore risultante sia riavviabile, ecco uno snippet di ordinamento che potrebbe essere utile:

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

uscite:

[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

Dalla documentazione ufficiale del tee :

In generale, se un iteratore utilizza la maggior parte o tutti i dati prima dell'avvio di un altro iteratore, è più veloce usare list () invece di tee ().

Quindi è meglio usare list(iterable)invece nel tuo caso.


6
che dire di infiniti generatori?
Dewfy

1
La velocità non è l'unica considerazione; list()mette l'intero iterabile in memoria
Chris_Rands

@Chris_Rands Così tee()se un iteratore consuma tutti i valori, è così che teefunziona.
AChampion l'

2
@Dewfy: per infiniti generatori, usa la soluzione di Aaron Digulla (funzione ExpensiveSetup che restituisce i dati preziosi.)
Jeff Learman,

3

Utilizzo di una funzione wrapper da gestire StopIteration

È possibile scrivere una funzione wrapper semplice per la funzione di generazione del generatore che tiene traccia quando il generatore è esaurito. Lo farà utilizzando l' StopIterationeccezione generata da un generatore quando raggiunge la fine dell'iterazione.

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

Come puoi notare sopra, quando la nostra funzione wrapper rileva StopIterationun'eccezione, reinizializza semplicemente l'oggetto generatore (usando un'altra istanza della chiamata di funzione).

E quindi, supponendo che tu definisca la tua funzione di fornitura del generatore da qualche parte come di seguito, potresti usare la sintassi del decoratore della funzione Python per avvolgerla implicitamente:

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

2

È possibile definire una funzione che restituisce il generatore

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

  return FunctionWithYield

Ora puoi fare tutte le volte che vuoi:

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

1
Grazie per la risposta, ma il punto principale della domanda era evitare la creazione , invocare la funzione interiore nasconde solo la creazione - la crei due volte
Dewfy

1

Non sono sicuro di cosa intendevi per preparazione costosa, ma suppongo che tu l'abbia fatto

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)

In tal caso, perché non riutilizzarlo data?


1

Non esiste alcuna opzione per ripristinare gli iteratori. L'iteratore di solito viene visualizzato quando scorre attraverso la next()funzione. L'unico modo è eseguire un backup prima di iterare sull'oggetto iteratore. Controlla sotto.

Creazione di un oggetto iteratore con elementi da 0 a 9

i=iter(range(10))

Scorrendo la funzione next () che verrà visualizzata

print(next(i))

Conversione dell'oggetto iteratore in elenco

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

quindi l'articolo 0 è già stato estratto. Inoltre, tutti gli elementi vengono visualizzati mentre convertiamo l'iteratore in elenco.

next(L) 

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

Quindi è necessario convertire l'iteratore in elenchi per il backup prima di iniziare l'iterazione. L'elenco potrebbe essere convertito in iteratore coniter(<list-object>)


1

Ora puoi usare more_itertools.seekable(uno strumento di terze parti) che abilita il ripristino degli iteratori.

Installa tramite > 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: il consumo di memoria aumenta durante l'avanzamento dell'iteratore, quindi fai attenzione ai grandi iterabili.


1

Puoi farlo usando itertools.cycle () puoi creare un iteratore con questo metodo e quindi eseguire un ciclo for sull'iteratore che eseguirà il ciclo sui suoi valori.

Per esempio:

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

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

genererà 20 numeri, da 0 a 4 ripetutamente.

Una nota dai documenti:

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

+1 perché funziona, ma vedo 2 problemi lì 1) grande footprint di memoria poiché la documentazione afferma "creare una copia" 2) Il ciclo infinito non è sicuramente quello che voglio
Dewfy

0

Ok, dici di voler chiamare un generatore più volte, ma l'inizializzazione è costosa ... Che ne dici di qualcosa del genere?

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

In alternativa, potresti semplicemente creare la tua classe che segue il protocollo iteratore e definisce una sorta di funzione di "reset".

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


Devi solo delegare il problema al wrapper. Supponiamo che una costosa inizializzazione crei un generatore. La mia domanda era su come ripristinare all'interno del tuo__call__
Dewfy

Aggiunto un secondo esempio in risposta al tuo commento. Questo essenzialmente è un generatore personalizzato con un metodo di ripristino.
tvt173,

0

La mia risposta risolve un problema leggermente diverso: se il generatore è costoso da inizializzare e ogni oggetto generato è costoso da generare. Ma dobbiamo consumare il generatore più volte in più funzioni. Per chiamare il generatore e ogni oggetto generato esattamente una volta possiamo usare i thread ed eseguire ciascuno dei metodi di consumo in thread diversi. Potremmo non raggiungere il vero parallelismo dovuto a GIL, ma raggiungeremo il nostro obiettivo.

Questo approccio ha fatto un buon lavoro nel seguente caso: il modello di apprendimento profondo elabora molte immagini. Il risultato sono molte maschere per molti oggetti sull'immagine. Ogni maschera consuma memoria. Abbiamo circa 10 metodi che rendono diverse statistiche e metriche, ma prendono tutte le immagini contemporaneamente. Tutte le immagini non possono adattarsi alla memoria. I moethod possono essere facilmente riscritti per accettare l'iteratore.

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

ussage:

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

Basta reinventare itertools.isliceo per async aiostream.stream.take, e questo post ti permette di farlo in asyn / attendono modo stackoverflow.com/a/42379188/149818
Dewfy

-3

Può essere fatto dall'oggetto codice. Ecco l'esempio.

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
bene, in realtà era necessario ripristinare il generatore per evitare due volte l'esecuzione del codice di inizializzazione. Il tuo approccio (1) esegue comunque l'inizializzazione due volte, (2) implica execche è leggermente sconsigliato per un caso così semplice.
Dewfy
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.