Sto iniziando a imparare Python e mi sono imbattuto in funzioni del generatore, quelle che contengono un'istruzione yield. Voglio sapere quali tipi di problemi queste funzioni sono davvero brave a risolvere.
Sto iniziando a imparare Python e mi sono imbattuto in funzioni del generatore, quelle che contengono un'istruzione yield. Voglio sapere quali tipi di problemi queste funzioni sono davvero brave a risolvere.
Risposte:
I generatori ti danno una valutazione pigra. Li usi iterando su di essi, esplicitamente con "for" o implicitamente passandolo a qualsiasi funzione o costrutto che itera. Puoi pensare ai generatori come a restituire più elementi, come se restituissero un elenco, ma invece di restituirli tutti in una volta li restituiscono uno per uno e la funzione del generatore viene sospesa fino a quando non viene richiesto l'elemento successivo.
I generatori sono utili per calcolare grandi insiemi di risultati (in particolare calcoli che coinvolgono i loop stessi) in cui non sai se avrai bisogno di tutti i risultati o dove non vuoi allo stesso tempo allocare la memoria per tutti i risultati . O per situazioni in cui il generatore utilizza un altro generatore o consuma qualche altra risorsa, ed è più conveniente se ciò è accaduto il più tardi possibile.
Un altro utilizzo per i generatori (che è davvero lo stesso) è sostituire i callback con l'iterazione. In alcune situazioni si desidera che una funzione svolga molto lavoro e occasionalmente riferisca al chiamante. Tradizionalmente useresti una funzione di callback per questo. Si passa questa richiamata alla funzione di lavoro e periodicamente la richiamerà. L'approccio del generatore è che la funzione lavoro (ora un generatore) non sa nulla della richiamata e si limita a cedere ogni volta che vuole segnalare qualcosa. Il chiamante, invece di scrivere una callback separata e passarla alla funzione lavoro, fa tutto il lavoro di reporting in un piccolo ciclo "for" attorno al generatore.
Ad esempio, supponi di aver scritto un programma di "ricerca nel file system". È possibile eseguire la ricerca nella sua interezza, raccogliere i risultati e quindi visualizzarli uno alla volta. Tutti i risultati dovrebbero essere raccolti prima di mostrare il primo, e tutti i risultati sarebbero in memoria allo stesso tempo. Oppure potresti visualizzare i risultati mentre li trovi, il che sarebbe più efficiente in termini di memoria e molto più amichevole per l'utente. Quest'ultimo potrebbe essere fatto passando la funzione di stampa dei risultati alla funzione di ricerca del file system, oppure potrebbe essere fatto semplicemente rendendo la funzione di ricerca un generatore e iterando sul risultato.
Se vuoi vedere un esempio degli ultimi due approcci, vedi os.path.walk () (la vecchia funzione di esplorazione del file system con callback) e os.walk () (il nuovo generatore di esplorazione del file system). Naturalmente, se volevi davvero raccogliere tutti i risultati in un elenco, l'approccio del generatore è banale da convertire all'approccio della grande lista:
big_list = list(the_generator)
yield
e join
dopo per ottenere il risultato successivo, non viene eseguito in parallelo (e nessun generatore di librerie standard lo fa; l'avvio segreto dei thread è disapprovato). Il generatore si ferma a ciascuno yield
fino a quando non viene richiesto il valore successivo. Se il generatore esegue il wrapping di I / O, il sistema operativo potrebbe memorizzare in modo proattivo i dati dal file presumendo che venga richiesto a breve, ma questo è il sistema operativo, Python non è coinvolto.
Uno dei motivi per utilizzare il generatore è rendere la soluzione più chiara per qualche tipo di soluzione.
L'altro è trattare i risultati uno alla volta, evitando di creare enormi elenchi di risultati che elaboreresti comunque separatamente.
Se hai una funzione fibonacci-up-n come questa:
# function version
def fibon(n):
a = b = 1
result = []
for i in xrange(n):
result.append(a)
a, b = b, a + b
return result
Puoi scrivere più facilmente la funzione in questo modo:
# generator version
def fibon(n):
a = b = 1
for i in xrange(n):
yield a
a, b = b, a + b
La funzione è più chiara. E se usi la funzione in questo modo:
for x in fibon(1000000):
print x,
in questo esempio, se si utilizza la versione del generatore, l'intero elenco di 1000000 elementi non verrà creato affatto, ma solo un valore alla volta. Questo non sarebbe il caso quando si utilizza la versione elenco, in cui verrà creato prima un elenco.
list(fibon(5))
Vedi la sezione "Motivazione" in PEP 255 .
Un uso non ovvio dei generatori è la creazione di funzioni interrompibili, che ti consente di fare cose come aggiornare l'interfaccia utente o eseguire diversi lavori "simultaneamente" (interleaved, in realtà) senza utilizzare i thread.
Trovo questa spiegazione che cancella il mio dubbio. Perché c'è una possibilità che anche la persona che non sa Generators
non sappiayield
Ritorno
L'istruzione return è dove tutte le variabili locali vengono distrutte e il valore risultante viene restituito (restituito) al chiamante. Se la stessa funzione viene chiamata qualche tempo dopo, la funzione otterrà un nuovo nuovo insieme di variabili.
dare la precedenza
Ma cosa succede se le variabili locali non vengono eliminate quando si esce da una funzione? Ciò implica che possiamo da resume the function
dove avevamo interrotto. È qui che viene generators
introdotto il concetto di e l' yield
affermazione riprende da dove era stata function
interrotta.
def generate_integers(N):
for i in xrange(N):
yield i
In [1]: gen = generate_integers(3)
In [2]: gen
<generator object at 0x8117f90>
In [3]: gen.next()
0
In [4]: gen.next()
1
In [5]: gen.next()
Quindi questa è la differenza tra le istruzioni return
e yield
in Python.
L'istruzione di rendimento è ciò che rende una funzione una funzione generatrice.
Quindi i generatori sono uno strumento semplice e potente per creare iteratori. Sono scritti come funzioni normali, ma usano l' yield
istruzione ogni volta che vogliono restituire dati. Ogni volta che next () viene chiamato, il generatore riprende da dove era stato interrotto (ricorda tutti i valori dei dati e quale istruzione è stata eseguita l'ultima volta).
Supponiamo che tu abbia 100 milioni di domini nella tua tabella MySQL e desideri aggiornare il ranking di Alexa per ogni dominio.
La prima cosa di cui hai bisogno è selezionare i tuoi nomi di dominio dal database.
Supponiamo che il nome della tua tabella sia domains
e il nome della colonna sia domain
.
Se lo usi SELECT domain FROM domains
, restituirà 100 milioni di righe che consumeranno molta memoria. Quindi il tuo server potrebbe bloccarsi.
Quindi hai deciso di eseguire il programma in batch. Supponiamo che la dimensione del nostro batch sia 1000.
Nel nostro primo batch interrogheremo le prime 1000 righe, controlleremo il ranking di Alexa per ogni dominio e aggiorneremo la riga del database.
Nel nostro secondo batch lavoreremo sulle prossime 1000 righe. Nel nostro terzo lotto sarà dal 2001 al 3000 e così via.
Ora abbiamo bisogno di una funzione di generatore che generi i nostri batch.
Ecco la nostra funzione di generatore:
def ResultGenerator(cursor, batchsize=1000):
while True:
results = cursor.fetchmany(batchsize)
if not results:
break
for result in results:
yield result
Come puoi vedere, la nostra funzione mantiene yield
i risultati. Se hai usato la parola chiave return
invece di yield
, l'intera funzione verrebbe terminata una volta raggiunta return.
return - returns only once
yield - returns multiple times
Se una funzione utilizza la parola chiave yield
, è un generatore.
Ora puoi iterare in questo modo:
db = MySQLdb.connect(host="localhost", user="root", passwd="root", db="domains")
cursor = db.cursor()
cursor.execute("SELECT domain FROM domains")
for result in ResultGenerator(cursor):
doSomethingWith(result)
db.close()
Buffering. Quando è efficiente recuperare i dati in blocchi di grandi dimensioni, ma elaborarli in piccoli blocchi, un generatore potrebbe aiutare:
def bufferedFetch():
while True:
buffer = getBigChunkOfData()
# insert some code to break on 'end of data'
for i in buffer:
yield i
Quanto sopra ti consente di separare facilmente il buffering dall'elaborazione. La funzione consumer può ora ottenere i valori uno per uno senza preoccuparsi del buffering.
Ho scoperto che i generatori sono molto utili per ripulire il codice e offrendoti un modo davvero unico per incapsulare e modularizzare il codice. In una situazione in cui hai bisogno di qualcosa per sputare costantemente valori in base alla sua elaborazione interna e quando quel qualcosa deve essere chiamato da qualsiasi punto del tuo codice (e non solo all'interno di un loop o di un blocco per esempio), i generatori sono la caratteristica per uso.
Un esempio astratto potrebbe essere un generatore di numeri di Fibonacci che non vive all'interno di un loop e quando viene chiamato da qualsiasi luogo restituirà sempre il numero successivo nella sequenza:
def fib():
first = 0
second = 1
yield first
yield second
while 1:
next = first + second
yield next
first = second
second = next
fibgen1 = fib()
fibgen2 = fib()
Ora hai due oggetti generatore di numeri di Fibonacci che puoi chiamare da qualsiasi punto del tuo codice e restituiranno sempre numeri di Fibonacci sempre più grandi in sequenza come segue:
>>> fibgen1.next(); fibgen1.next(); fibgen1.next(); fibgen1.next()
0
1
1
2
>>> fibgen2.next(); fibgen2.next()
0
1
>>> fibgen1.next(); fibgen1.next()
3
5
La cosa bella dei generatori è che incapsulano lo stato senza dover passare attraverso i cerchi della creazione di oggetti. Un modo di pensarli è come "funzioni" che ricordano il loro stato interno.
Ho ottenuto l'esempio di Fibonacci da Python Generators: cosa sono? e con un po 'di immaginazione, puoi inventare molte altre situazioni in cui i generatori rappresentano un'ottima alternativa ai for
loop e ad altri costrutti di iterazione tradizionali.
La semplice spiegazione: considera una for
dichiarazione
for item in iterable:
do_stuff()
La maggior parte delle volte, iterable
non è necessario che tutti gli elementi siano presenti dall'inizio, ma possono essere generati al volo in base alle esigenze. Questo può essere molto più efficiente in entrambi
Altre volte, non conosci nemmeno tutti gli elementi in anticipo. Per esempio:
for command in user_input():
do_stuff_with(command)
Non hai modo di conoscere in anticipo tutti i comandi dell'utente, ma puoi usare un bel ciclo come questo se hai un generatore che ti consegna i comandi:
def user_input():
while True:
wait_for_command()
cmd = get_command()
yield cmd
Con i generatori puoi anche avere iterazioni su sequenze infinite, il che ovviamente non è possibile quando iterazioni su contenitori.
I miei usi preferiti sono le operazioni di "filtro" e "riduzione".
Diciamo che stiamo leggendo un file e vogliamo solo le righe che iniziano con "##".
def filter2sharps( aSequence ):
for l in aSequence:
if l.startswith("##"):
yield l
Possiamo quindi utilizzare la funzione generatore in un ciclo appropriato
source= file( ... )
for line in filter2sharps( source.readlines() ):
print line
source.close()
L'esempio di riduzione è simile. Supponiamo di avere un file in cui dobbiamo individuare blocchi di <Location>...</Location>
linee. [Non tag HTML, ma righe che sembrano simili a tag.]
def reduceLocation( aSequence ):
keep= False
block= None
for line in aSequence:
if line.startswith("</Location"):
block.append( line )
yield block
block= None
keep= False
elif line.startsWith("<Location"):
block= [ line ]
keep= True
elif keep:
block.append( line )
else:
pass
if block is not None:
yield block # A partial block, icky
Di nuovo, possiamo usare questo generatore in un ciclo for corretto.
source = file( ... )
for b in reduceLocation( source.readlines() ):
print b
source.close()
L'idea è che una funzione generatore ci permette di filtrare o ridurre una sequenza, producendo un'altra sequenza un valore alla volta.
fileobj.readlines()
leggerebbe l'intero file in un elenco in memoria, vanificando lo scopo di utilizzare i generatori. Poiché gli oggetti file sono già iterabili, puoi usare for b in your_generator(fileobject):
invece. In questo modo il file verrà letto una riga alla volta, per evitare di leggere l'intero file.
Un esempio pratico in cui potresti utilizzare un generatore è se hai un qualche tipo di forma e vuoi iterare sui suoi angoli, bordi o altro. Per il mio progetto (codice sorgente qui ) avevo un rettangolo:
class Rect():
def __init__(self, x, y, width, height):
self.l_top = (x, y)
self.r_top = (x+width, y)
self.r_bot = (x+width, y+height)
self.l_bot = (x, y+height)
def __iter__(self):
yield self.l_top
yield self.r_top
yield self.r_bot
yield self.l_bot
Ora posso creare un rettangolo e passare ai suoi angoli:
myrect=Rect(50, 50, 100, 100)
for corner in myrect:
print(corner)
Invece di __iter__
te potresti avere un metodo iter_corners
e chiamarlo con for corner in myrect.iter_corners()
. È solo più elegante da usare __iter__
poiché possiamo usare il nome dell'istanza della classe direttamente for
nell'espressione.
Alcune buone risposte qui, tuttavia, consiglierei anche una lettura completa del tutorial sulla programmazione funzionale di Python che aiuta a spiegare alcuni dei casi d'uso più potenti dei generatori.
Poiché il metodo di invio di un generatore non è stato menzionato, ecco un esempio:
def test():
for i in xrange(5):
val = yield
print(val)
t = test()
# Proceed to 'yield' statement
next(t)
# Send value to yield
t.send(1)
t.send('2')
t.send([3])
Mostra la possibilità di inviare un valore a un generatore in funzione. Un corso più avanzato sui generatori nel video qui sotto (incluso yield
da esplorazione, generatori per l'elaborazione parallela, sfuggire al limite di ricorsione, ecc.)
Mucchi di roba. Ogni volta che desideri generare una sequenza di elementi, ma non vuoi doverli "materializzare" tutti in un elenco contemporaneamente. Ad esempio, potresti avere un semplice generatore che restituisce numeri primi:
def primes():
primes_found = set()
primes_found.add(2)
yield 2
for i in itertools.count(1):
candidate = i * 2 + 1
if not all(candidate % prime for prime in primes_found):
primes_found.add(candidate)
yield candidate
È quindi possibile utilizzarlo per generare i prodotti dei numeri primi successivi:
def prime_products():
primeiter = primes()
prev = primeiter.next()
for prime in primeiter:
yield prime * prev
prev = prime
Questi sono esempi abbastanza banali, ma puoi vedere come può essere utile per elaborare set di dati di grandi dimensioni (potenzialmente infiniti!) Senza generarli in anticipo, che è solo uno degli usi più ovvi.
Ottimo anche per stampare i numeri primi fino an:
def genprime(n=10):
for num in range(3, n+1):
for factor in range(2, num):
if num%factor == 0:
break
else:
yield(num)
for prime_num in genprime(100):
print(prime_num)