Comprensione dei generatori in Python


218

Al momento sto leggendo il ricettario di Python e attualmente sto guardando i generatori. Sto trovando difficile farmi girare la testa.

Dato che provengo da uno sfondo Java, esiste un equivalente Java? Il libro parlava di "Produttore / Consumatore", tuttavia quando sento che penso al threading.

Che cos'è un generatore e perché lo useresti? Senza citare alcun libro, ovviamente (a meno che tu non riesca a trovare una risposta decente e semplicistica direttamente da un libro). Forse con esempi, se ti senti generoso!

Risposte:


402

Nota: questo post presuppone la sintassi di Python 3.x.

Un generatore è semplicemente una funzione che restituisce un oggetto su cui è possibile chiamare next, in modo tale che per ogni chiamata restituisca un valore, fino a quando non genera StopIterationun'eccezione, segnalando che tutti i valori sono stati generati. Tale oggetto è chiamato iteratore .

Le normali funzioni restituiscono un singolo valore usando return, proprio come in Java. In Python, tuttavia, esiste un'alternativa, chiamata yield. L'uso yieldovunque in una funzione lo rende un generatore. Rispettare questo codice:

>>> def myGen(n):
...     yield n
...     yield n + 1
... 
>>> g = myGen(6)
>>> next(g)
6
>>> next(g)
7
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Come puoi vedere, myGen(n)è una funzione che produce ne n + 1. Ogni chiamata a nextproduce un singolo valore, fino a quando tutti i valori sono stati ceduti. fori loop chiamano nextin background, quindi:

>>> for n in myGen(6):
...     print(n)
... 
6
7

Allo stesso modo ci sono espressioni del generatore , che forniscono un mezzo per descrivere in modo succinto alcuni tipi comuni di generatori:

>>> g = (n for n in range(3, 5))
>>> next(g)
3
>>> next(g)
4
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Nota che le espressioni del generatore sono molto simili alla comprensione dell'elenco :

>>> lc = [n for n in range(3, 5)]
>>> lc
[3, 4]

Osservare che un oggetto generatore viene generato una volta , ma il suo codice non viene eseguito tutto in una volta. Chiama solo per nexteseguire effettivamente (parte del) codice. L'esecuzione del codice in un generatore si interrompe una volta yieldraggiunta un'istruzione, sulla quale restituisce un valore. La chiamata successiva a nextcausa quindi l'esecuzione dell'esecuzione nello stato in cui il generatore è stato lasciato dopo l'ultimo yield. Questa è una differenza fondamentale con le normali funzioni: quelle iniziano sempre a essere eseguite "in alto" e scartano il loro stato quando restituiscono un valore.

Ci sono più cose da dire su questo argomento. Ad esempio è possibile sendtornare ai dati in un generatore ( riferimento ). Ma è qualcosa che ti suggerisco di non esaminare fino a quando non capisci il concetto di base di un generatore.

Ora potresti chiedere: perché usare i generatori? Ci sono un paio di buoni motivi:

  • Alcuni concetti possono essere descritti in modo molto più succinto usando i generatori.
  • Invece di creare una funzione che restituisce un elenco di valori, si può scrivere un generatore che genera i valori al volo. Ciò significa che non è necessario costruire alcun elenco, il che significa che il codice risultante è più efficiente in termini di memoria. In questo modo si possono anche descrivere flussi di dati che sarebbero semplicemente troppo grandi per adattarsi alla memoria.
  • I generatori consentono un modo naturale per descrivere flussi infiniti . Considera ad esempio i numeri di Fibonacci :

    >>> def fib():
    ...     a, b = 0, 1
    ...     while True:
    ...         yield a
    ...         a, b = b, a + b
    ... 
    >>> import itertools
    >>> list(itertools.islice(fib(), 10))
    [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

    Questo codice usa itertools.isliceper prendere un numero finito di elementi da un flusso infinito. Si consiglia di dare una buona occhiata alle funzioni del itertoolsmodulo, in quanto sono strumenti essenziali per scrivere generatori avanzati con grande facilità.


   Informazioni su Python <= 2.6: negli esempi sopra nextè una funzione che chiama il metodo __next__sull'oggetto dato. In Python <= 2.6 si usa una tecnica leggermente diversa, vale a dire o.next()invece di next(o). Python 2.7 ha next()call, .nextquindi non è necessario utilizzare quanto segue in 2.7:

>>> g = (n for n in range(3, 5))
>>> g.next()
3

9
Dici che è possibile senddati ad un generatore. Una volta che lo fai, hai un 'coroutine'. È molto semplice implementare modelli come il menzionato consumatore / produttore con coroutine perché non hanno bisogno di se Lockquindi non possono bloccarsi. È difficile descrivere le coroutine senza battere i fili, quindi dirò solo che le coroutine sono un'alternativa molto elegante al threading.
Jochen Ritzel,

I generatori di Python sono fondamentalmente macchine di Turing in termini di come funzionano?
Fiery Phoenix,

48

Un generatore è effettivamente una funzione che restituisce (dati) prima che sia terminata, ma si ferma in quel punto e puoi riprendere la funzione in quel punto.

>>> def myGenerator():
...     yield 'These'
...     yield 'words'
...     yield 'come'
...     yield 'one'
...     yield 'at'
...     yield 'a'
...     yield 'time'

>>> myGeneratorInstance = myGenerator()
>>> next(myGeneratorInstance)
These
>>> next(myGeneratorInstance)
words

e così via. Il (o uno) vantaggio dei generatori è che poiché trattano i dati un pezzo alla volta, è possibile gestire grandi quantità di dati; con gli elenchi, requisiti di memoria eccessivi potrebbero diventare un problema. I generatori, proprio come gli elenchi, sono iterabili, quindi possono essere utilizzati nello stesso modo:

>>> for word in myGeneratorInstance:
...     print word
These
words
come
one
at 
a 
time

Si noti che i generatori forniscono un altro modo per gestire l'infinito, ad esempio

>>> from time import gmtime, strftime
>>> def myGen():
...     while True:
...         yield strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())    
>>> myGeneratorInstance = myGen()
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:17:15 +0000
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:18:02 +0000   

Il generatore incapsula un ciclo infinito, ma questo non è un problema perché ricevi ogni risposta solo ogni volta che lo chiedi.


30

Prima di tutto, il termine generatore in origine era in qualche modo mal definito in Python, causando molta confusione. Probabilmente intendi iteratori e iterabili (vedi qui ). Quindi in Python ci sono anche funzioni di generatore (che restituiscono un oggetto generatore), oggetti di generatore (che sono iteratori) ed espressioni di generatore (che vengono valutate su un oggetto generatore).

Secondo la voce del glossario per generatore sembra che la terminologia ufficiale sia ora che generatore è l'abbreviazione di "funzione generatore". In passato la documentazione definiva i termini in modo incoerente, ma per fortuna questo è stato corretto.

Potrebbe essere comunque una buona idea essere precisi ed evitare il termine "generatore" senza ulteriori specifiche.


2
Hmm penso che tu abbia ragione, almeno secondo un test di alcune righe in Python 2.6. Un'espressione di generatore restituisce un iteratore (noto anche come "oggetto generatore"), non un generatore.
Craig McQueen,

22

I generatori potrebbero essere considerati una scorciatoia per la creazione di un iteratore. Si comportano come un Iteratore Java. Esempio:

>>> g = (x for x in range(10))
>>> g
<generator object <genexpr> at 0x7fac1c1e6aa0>
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> list(g)   # force iterating the rest
[3, 4, 5, 6, 7, 8, 9]
>>> g.next()  # iterator is at the end; calling next again will throw
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Spero che questo aiuti / sia quello che stai cercando.

Aggiornare:

Come molte altre risposte stanno mostrando, ci sono diversi modi per creare un generatore. Puoi usare la sintassi tra parentesi come nel mio esempio sopra, oppure puoi usare la resa. Un'altra caratteristica interessante è che i generatori possono essere "infiniti" - iteratori che non si fermano:

>>> def infinite_gen():
...     n = 0
...     while True:
...         yield n
...         n = n + 1
... 
>>> g = infinite_gen()
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> g.next()
3
...

1
Ora, Java ha Streams, che sono molto più simili ai generatori, tranne per il fatto che apparentemente non puoi semplicemente ottenere l'elemento successivo senza una sorprendente quantità di problemi.
Finanzi la causa di Monica il

12

Non esiste un equivalente Java.

Ecco un piccolo esempio inventato:

#! /usr/bin/python
def  mygen(n):
    x = 0
    while x < n:
        x = x + 1
        if x % 3 == 0:
            yield x

for a in mygen(100):
    print a

C'è un loop nel generatore che va da 0 a n e se la variabile loop è un multiplo di 3, produce la variabile.

Durante ogni iterazione del forloop viene eseguito il generatore. Se è la prima volta che il generatore viene eseguito, inizia all'inizio, altrimenti continua dalla volta precedente in cui ha ceduto.


2
L'ultimo paragrafo è molto importante: lo stato della funzione generatore viene "congelato" ogni volta che produce sth e continua esattamente nello stesso stato quando viene invocato la volta successiva.
Johannes Charra,

Non esiste un equivalente sintattico in Java di una "espressione di generatore", ma i generatori - una volta che ne hai uno - sono essenzialmente solo un iteratore (stesse caratteristiche di base di un iteratore Java).
ripensare il

@overthink: Beh, i generatori possono avere altri effetti collaterali che gli iteratori Java non possono avere. Se dovessi inserire print "hello"il x=x+1mio esempio, "ciao" verrebbe stampato 100 volte, mentre il corpo del ciclo for verrebbe eseguito solo 33 volte.
Wernsey,

@iWerner: Abbastanza sicuro che si potrebbe avere lo stesso effetto in Java. L'implementazione di next () nell'iteratore Java equivalente dovrebbe comunque cercare da 0 a 99 (usando il tuo esempio mygen (100)), quindi puoi System.out.println () ogni volta che vuoi. Tuttavia, verrai restituito solo 33 volte da next (). Ciò che manca a Java è la sintassi del rendimento molto utile che è significativamente più facile da leggere (e scrivere).
ripensare il

Mi è piaciuto leggere e ricordare questa def di una riga: se è la prima volta che il generatore viene eseguito, inizia all'inizio, altrimenti continua dalla volta precedente.
Iqra.

8

Mi piace descrivere i generatori, a quelli con un discreto background in linguaggi di programmazione e informatica, in termini di stack frame.

In molte lingue, c'è uno stack in cima al quale è lo stack "frame" corrente. Il frame dello stack include lo spazio allocato per le variabili locali alla funzione, inclusi gli argomenti passati a tale funzione.

Quando si chiama una funzione, l'attuale punto di esecuzione (il "contatore del programma" o equivalente) viene inserito nello stack e viene creato un nuovo frame dello stack. L'esecuzione quindi trasferisce all'inizio della funzione chiamata.

Con funzioni regolari, ad un certo punto la funzione restituisce un valore e lo stack viene "espulso". Il frame dello stack della funzione viene scartato e l'esecuzione riprende nella posizione precedente.

Quando una funzione è un generatore, può restituire un valore senza scartare il frame dello stack, usando l'istruzione yield. I valori delle variabili locali e il contatore del programma all'interno della funzione vengono conservati. Ciò consente al generatore di essere ripreso in un secondo momento, con l'esecuzione che continua dall'istruzione yield e può eseguire più codice e restituire un altro valore.

Prima di Python 2.5 questo era tutto ciò che i generatori facevano. Pitone 2.5 aggiunto la possibilità di passare valori torna in al generatore pure. In tal modo, il valore passato è disponibile come espressione risultante dall'istruzione yield che aveva temporaneamente restituito il controllo (e un valore) dal generatore.

Il vantaggio chiave per i generatori è che lo "stato" della funzione viene preservato, diversamente dalle normali funzioni in cui ogni volta che il frame dello stack viene scartato, si perde tutto quello "stato". Un vantaggio secondario è che parte dell'overhead di chiamata di funzione (creazione ed eliminazione di frame di stack) viene evitato, sebbene questo sia di solito un vantaggio minore.


7

Aiuta a fare una chiara distinzione tra la funzione foo e il generatore foo (n):

def foo(n):
    yield n
    yield n+1

foo è una funzione. foo (6) è un oggetto generatore.

Il modo tipico di utilizzare un oggetto generatore è in un ciclo:

for n in foo(6):
    print(n)

Il ciclo stampa

# 6
# 7

Pensa a un generatore come a una funzione ripristinabile.

yieldsi comporta come returnnel senso che i valori prodotti vengono "restituiti" dal generatore. A differenza del ritorno, tuttavia, la volta successiva che viene richiesto un valore al generatore, la funzione del generatore, foo, riprende da dove si era interrotta - dopo l'ultima dichiarazione di rendimento - e continua a funzionare fino a quando non colpisce un'altra dichiarazione di rendimento.

Dietro le quinte, quando si chiama bar=foo(6)la barra degli oggetti del generatore viene definito per avere un nextattributo.

Puoi chiamarlo tu stesso per recuperare i valori ottenuti da foo:

next(bar)    # Works in Python 2.6 or Python 3.x
bar.next()   # Works in Python 2.5+, but is deprecated. Use next() if possible.

Quando foo termina (e non ci sono più valori next(bar)resi ), la chiamata genera un errore StopInteration.


6

L'unica cosa che posso aggiungere alla risposta di Stephan202 è la raccomandazione di dare un'occhiata alla presentazione PyCon '08 di David Beazley "Generator Tricks for Systems Programmers", che è la migliore spiegazione singola di come e perché dei generatori che ho visto dovunque. Questa è la cosa che mi ha portato da "Python sembra piuttosto divertente" a "Questo è quello che stavo cercando." È all'indirizzo http://www.dabeaz.com/generators/ .


5

Questo post userà i numeri di Fibonacci come strumento per sviluppare l'utilità dei generatori Python .

Questo post conterrà sia il codice C ++ che Python.

I numeri di Fibonacci sono definiti come la sequenza: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ....

O in generale:

F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2

Questo può essere trasferito in una funzione C ++ in modo estremamente semplice:

size_t Fib(size_t n)
{
    //Fib(0) = 0
    if(n == 0)
        return 0;

    //Fib(1) = 1
    if(n == 1)
        return 1;

    //Fib(N) = Fib(N-2) + Fib(N-1)
    return Fib(n-2) + Fib(n-1);
}

Ma se vuoi stampare i primi sei numeri di Fibonacci, ricalcolerai molti valori con la funzione sopra.

Ad esempio :, Fib(3) = Fib(2) + Fib(1)ma Fib(2)ricalcola anche Fib(1). Più alto è il valore che vuoi calcolare, peggio sarai.

Quindi si può essere tentati di riscrivere quanto sopra tenendo traccia dello stato in main.

// Not supported for the first two elements of Fib
size_t GetNextFib(size_t &pp, size_t &p)
{
    int result = pp + p;
    pp = p;
    p = result;
    return result;
}

int main(int argc, char *argv[])
{
    size_t pp = 0;
    size_t p = 1;
    std::cout << "0 " << "1 ";
    for(size_t i = 0; i <= 4; ++i)
    {
        size_t fibI = GetNextFib(pp, p);
        std::cout << fibI << " ";
    }
    return 0;
}

Ma questo è molto brutto e complica la nostra logica main. Sarebbe meglio non doversi preoccupare dello stato nella nostra mainfunzione.

Potremmo restituire a vectordi valori e usare a iteratorper scorrere su quel set di valori, ma ciò richiede molta memoria tutta in una volta per un gran numero di valori di ritorno.

Quindi, tornando al nostro vecchio approccio, cosa succede se volessimo fare qualcos'altro oltre a stampare i numeri? Dovremmo copiare e incollare l'intero blocco di codice maine modificare le istruzioni di output in qualsiasi altra cosa volessimo fare. E se copi e incolli il codice, dovresti sparare. Non vuoi spararti, vero?

Per risolvere questi problemi ed evitare di essere colpiti, possiamo riscrivere questo blocco di codice usando una funzione di callback. Ogni volta che si incontra un nuovo numero di Fibonacci, chiamiamo la funzione di richiamata.

void GetFibNumbers(size_t max, void(*FoundNewFibCallback)(size_t))
{
    if(max-- == 0) return;
    FoundNewFibCallback(0);
    if(max-- == 0) return;
    FoundNewFibCallback(1);

    size_t pp = 0;
    size_t p = 1;
    for(;;)
    {
        if(max-- == 0) return;
        int result = pp + p;
        pp = p;
        p = result;
        FoundNewFibCallback(result);
    }
}

void foundNewFib(size_t fibI)
{
    std::cout << fibI << " ";
}

int main(int argc, char *argv[])
{
    GetFibNumbers(6, foundNewFib);
    return 0;
}

Questo è chiaramente un miglioramento, la tua logica mainnon è così ingombra e puoi fare tutto ciò che vuoi con i numeri di Fibonacci, semplicemente definire nuovi callback.

Ma questo non è ancora perfetto. E se volessi ottenere solo i primi due numeri di Fibonacci e poi fare qualcosa, poi prenderne ancora un po ', quindi fare qualcos'altro?

Bene, potremmo continuare come siamo stati, e potremmo ricominciare ad aggiungere stato main, permettendo a GetFibNumbers di partire da un punto arbitrario. Ma questo gonfia ulteriormente il nostro codice e sembra già troppo grande per un compito semplice come la stampa dei numeri di Fibonacci.

Potremmo implementare un modello produttore e consumatore tramite un paio di thread. Ma questo complica ancora di più il codice.

Parliamo invece di generatori.

Python ha una funzione linguistica molto bella che risolve problemi come questi chiamati generatori.

Un generatore ti consente di eseguire una funzione, fermarti in un punto arbitrario e poi continuare di nuovo da dove eri rimasto. Ogni volta che restituisce un valore.

Considera il seguente codice che utilizza un generatore:

def fib():
    pp, p = 0, 1
    while 1:
        yield pp
        pp, p = p, pp+p

g = fib()
for i in range(6):
    g.next()

Che ci dà i risultati:

0 1 1 2 3 5

L' yieldistruzione viene utilizzata in congiunzione con i generatori Python. Salva lo stato della funzione e restituisce il valore generato. La prossima volta che chiamate la funzione next () sul generatore, continuerà da dove la resa è stata interrotta.

Questo è di gran lunga più pulito del codice della funzione di callback. Abbiamo un codice più pulito, un codice più piccolo e per non parlare di un codice molto più funzionale (Python consente numeri interi arbitrariamente grandi).

fonte


3

Credo che la prima apparizione di iteratori e generatori sia stata nel linguaggio di programmazione Icon, circa 20 anni fa.

Potresti goderti la panoramica di Icon , che ti consente di avvolgere la testa attorno a loro senza concentrarti sulla sintassi (poiché Icon è una lingua che probabilmente non conosci, e Griswold stava spiegando i vantaggi della sua lingua alle persone che provengono da altre lingue).

Dopo aver letto solo alcuni paragrafi, l'utilità di generatori e iteratori potrebbe diventare più evidente.


2

L'esperienza con la comprensione degli elenchi ha dimostrato la loro diffusa utilità in Python. Tuttavia, molti casi d'uso non devono avere un elenco completo creato in memoria. Invece, devono solo scorrere gli elementi uno alla volta.

Ad esempio, il seguente codice di sommatoria costruirà un elenco completo di quadrati in memoria, ripeterà questi valori e, quando il riferimento non è più necessario, eliminerà l'elenco:

sum([x*x for x in range(10)])

La memoria viene conservata utilizzando invece un'espressione del generatore:

sum(x*x for x in range(10))

Vantaggi simili sono conferiti ai costruttori per gli oggetti contenitore:

s = Set(word  for line in page  for word in line.split())
d = dict( (k, func(k)) for k in keylist)

Le espressioni del generatore sono particolarmente utili con funzioni come sum (), min () e max () che riducono un input iterabile a un singolo valore:

max(len(line)  for line in file  if line.strip())

Di Più


2

Ho messo questo pezzo di codice che spiega 3 concetti chiave sui generatori:

def numbers():
    for i in range(10):
            yield i

gen = numbers() #this line only returns a generator object, it does not run the code defined inside numbers

for i in gen: #we iterate over the generator and the values are printed
    print(i)

#the generator is now empty

for i in gen: #so this for block does not print anything
    print(i)
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.