Per cosa puoi usare le funzioni del generatore Python?
Domanda
Sto iniziando a imparare Python e ho scoperto le funzioni del generatore, quelle che hanno una dichiarazione di rendimento in esse. Voglio sapere quali tipi di problemi sono veramente bravi a risolvere queste funzioni.
Soluzione
I generatori ti danno una valutazione pigra. Li usi ripetendo 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 generatore viene messa in pausa fino a quando non viene richiesto l'elemento successivo.
I generatori sono utili per il calcolo di grandi serie di risultati (in particolare calcoli che coinvolgono i cicli stessi) in cui non si sa se saranno necessari tutti i risultati o in cui non si desidera allocare la memoria per tutti i risultati in lo stesso tempo. O per situazioni in cui il generatore utilizza un altro generatore o consuma qualche altra risorsa, ed è più conveniente se ciò avvenisse il più tardi possibile.
Un altro uso per i generatori (che è davvero lo stesso) è sostituire i callback con iterazione. In alcune situazioni si desidera che una funzione svolga molto lavoro e che occasionalmente riferisca al chiamante. Tradizionalmente useresti una funzione di callback per questo. Si passa questo callback alla funzione di lavoro e periodicamente si chiama questo callback. L'approccio del generatore è che la funzione di lavoro (ora un generatore) non sa nulla del callback e si limita a cedere ogni volta che vuole segnalare qualcosa. Il chiamante, invece di scrivere un callback separato e passarlo alla funzione di lavoro, fa tutto il lavoro di reporting in un piccolo ciclo "for" attorno al generatore.
Ad esempio, supponiamo che tu abbia scritto un programma di "ricerca nel filesystem". È possibile eseguire la ricerca nella sua interezza, raccogliere i risultati e visualizzarli uno alla volta. Tutti i risultati dovrebbero essere raccolti prima che tu mostrassi il primo, e tutti i risultati sarebbero in memoria contemporaneamente. 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 filesystem, oppure potrebbe essere fatto semplicemente trasformando la funzione di ricerca in un generatore e ripetendo il risultato.
Se vuoi vedere un esempio di questi ultimi due approcci, vedi os.path.walk () (la vecchia funzione di filesystem-walking con callback) e os.walk () (il nuovo generatore di filesystem-walking.) Di ovviamente, se davvero volessi raccogliere tutti i risultati in un elenco, l'approccio del generatore è banale da convertire in un approccio a grande elenco:
big_list = list(the_generator)
Altri suggerimenti
Uno dei motivi per usare il generatore è quello di rendere più chiara la soluzione per un qualche tipo di soluzione.
L'altro è quello di trattare i risultati uno alla volta, evitando di creare enormi elenchi di risultati che elaboreresti comunque separati.
Se hai una funzione fibonacci-up-to-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 articoli 1000000 non verrà creato affatto, ma solo un valore alla volta. Questo non sarebbe il caso quando si utilizza la versione dell'elenco, in cui un elenco verrà creato per primo.
Vedi " Motivazione " sezione in PEP 255 .
Un uso non ovvio di generatori sta creando funzioni interrompibili, che ti permettono di fare cose come aggiornare l'interfaccia utente o eseguire diversi lavori " contemporaneamente " (interfogliato, in realtà) mentre non si usano i thread.
Trovo questa spiegazione che chiarisce i miei dubbi. Perché esiste la possibilità che quella persona che non conosca Generators
non sia a conoscenza di yield
Return
L'istruzione return è dove tutte le variabili locali vengono distrutte e il valore risultante viene restituito (restituito) al chiamante. Se la stessa funzione verrà chiamata qualche tempo dopo, la funzione otterrà un nuovo set di variabili.
Resa
Ma cosa succede se le variabili locali non vengono eliminate quando si esce da una funzione? Ciò implica che possiamo resume the function
da dove eravamo rimasti. Qui è dove viene introdotto il concetto di generators
e l'istruzione function
riprende da dove return
è stato interrotto.
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 <=> e <=> istruzioni in Python.
Dichiarazione di rendimento è ciò che rende una funzione una funzione generatore.
Quindi i generatori sono uno strumento semplice e potente per la creazione di iteratori. Sono scritti come normali funzioni, ma usano l'istruzione <=> ogni volta che vogliono restituire dati. Ogni volta che viene chiamato next (), il generatore riprende da dove era stato interrotto (ricorda tutti i valori dei dati e quale istruzione è stata eseguita l'ultima volta).
Esempio di mondo reale
Supponiamo che tu abbia 100 milioni di domini nella tua tabella MySQL e che desideri aggiornare il ranking Alexa per ogni dominio.
La prima cosa che devi fare è selezionare i nomi dei tuoi domini dal database.
Supponiamo che il nome della tua tabella sia domains
e il nome della colonna sia domain
.
Se 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 lotto sia 1000.
Nel nostro primo batch interrogheremo le prime 1000 righe, controlleremo il ranking di Alexa per ciascun 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 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 si utilizzava la parola chiave return
anziché <=>, l'intera funzione sarebbe terminata una volta raggiunta la restituzione.
return - returns only once
yield - returns multiple times
Se una funzione utilizza la parola chiave <=>, allora è 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 blocchi di piccole dimensioni, 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 consente di separare facilmente il buffering dall'elaborazione. La funzione consumer ora può 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 di incapsulare e modularizzare il codice. In una situazione in cui hai bisogno di qualcosa per sputare costantemente valori basati sulla sua stessa elaborazione interna e quando quel qualcosa deve essere chiamato da qualsiasi parte del tuo codice (e non solo all'interno di un ciclo o di un blocco, ad esempio), i generatori sono la funzionalità da utilizzare.
Un esempio astratto potrebbe essere un generatore di numeri di Fibonacci che non vive in un ciclo 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 Fibonacci che puoi chiamare da qualsiasi parte del codice e restituiranno sempre numeri 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 pensarci è come & Quot; funzioni & Quot; che ricordano il loro stato interno.
Ho preso l'esempio di Fibonacci da Python Generators - What sono? e con un po 'di immaginazione, puoi trovare molte altre situazioni in cui i generatori rappresentano un'ottima alternativa ai for
loop e altri costrutti di iterazione tradizionali.
La semplice spiegazione:
Prendi in considerazione una for
dichiarazione
for item in iterable:
do_stuff()
Molte volte, non è necessario che tutti gli elementi in iterable
siano presenti fin dall'inizio, ma possono essere generati al volo quando richiesti. Questo può essere molto più efficiente in entrambi
- spazio (non è mai necessario archiviare tutti gli oggetti contemporaneamente) e
- tempo (l'iterazione potrebbe terminare prima che tutti gli elementi siano necessari).
Altre volte, non si conoscono nemmeno tutti gli elementi in anticipo. Ad 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 da 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 si scorre su container.
I miei usi preferiti sono " filtro " e " ridurre " operazioni.
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 del generatore in un ciclo corretto
source= file( ... )
for line in filter2sharps( source.readlines() ):
print line
source.close()
L'esempio di riduzione è simile. Supponiamo di avere un file in cui è necessario individuare blocchi di <Location>...</Location>
righe. [Non tag HTML, ma linee che sembrano tag-like.]
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
Ancora una volta, possiamo usare questo generatore in un vero ciclo per.
source = file( ... )
for b in reduceLocation( source.readlines() ):
print b
source.close()
L'idea è che una funzione di generatore ci consente di filtrare o ridurre una sequenza, producendo un'altra sequenza un valore alla volta.
Un esempio pratico in cui è possibile utilizzare un generatore è se si dispone di un qualche tipo di forma e si desidera iterare su 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 avvolgere i suoi angoli:
myrect=Rect(50, 50, 100, 100)
for corner in myrect:
print(corner)
Invece di __iter__
potresti avere un metodo iter_corners
e chiamarlo con for corner in myrect.iter_corners()
. È più elegante usare for
da allora possiamo usare il nome dell'istanza della classe direttamente nell'espressione <=>.
Alcune buone risposte qui, tuttavia, consiglierei anche una lettura completa di Python Tutorial sulla programmazione funzionale che aiuta a spiegare alcuni dei più potenti casi d'uso dei generatori.
- Particolarmente interessante è che ora è possibile aggiorna la variabile di rendimento dall'esterno della funzione generatore , rendendo così possibile la creazione di coroutine dinamiche e intrecciate con relativamente poco sforzo.
- Vedi anche PEP 342: Coroutine tramite Enhanced Generators per ulteriori informazioni .
Uso i generatori quando il nostro server web funge da proxy:
- Il client richiede un URL proxy dal server
- Il server inizia a caricare l'URL di destinazione
- Il server cede a restituire i risultati al client non appena li ottiene
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 esecuzione. Un corso più avanzato sui generatori nel video qui sotto (inclusi yield
da spiegazione, generatori per l'elaborazione parallela, sfuggendo al limite di ricorsione, ecc.)
Mucchi di roba. Ogni volta che desideri generare una sequenza di elementi, ma non devi "materializzarli" tutti in un elenco in una volta. 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
Potresti quindi usarlo 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 grandi set di dati (potenzialmente infiniti!) senza generarli in anticipo, che è solo uno degli usi più ovvi.
Buono anche per stampare i numeri primi fino a n:
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)