Per cosa puoi usare le funzioni del generatore Python?


215

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.


6
forse una domanda migliore sarebbe quando non dovremmo usarli
cregox

1
Esempio del mondo reale qui
Giri

Risposte:


241

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)

Un generatore come quello che produce elenchi di file system esegue azioni in parallelo al codice che esegue quel generatore in un ciclo? Idealmente il computer eseguirà il corpo del ciclo (elaborando l'ultimo risultato) mentre contemporaneamente fa tutto ciò che il generatore deve fare per ottenere il valore successivo.
Steven Lu

@StevenLu: a meno che non si prenda la briga di avviare manualmente i thread prima yielde joindopo 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 yieldfino 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.
ShadowRanger

90

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.


18
e se hai bisogno di una lista, puoi sempre farlolist(fibon(5))
endolith

41

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.


1
La sezione Motivation è carina in quanto contiene un esempio specifico: "Quando una funzione producer ha un lavoro abbastanza difficile da richiedere il mantenimento dello stato tra i valori prodotti, la maggior parte dei linguaggi di programmazione non offre alcuna soluzione piacevole ed efficiente oltre all'aggiunta di una funzione di callback all'argomento del produttore list ... Ad esempio, tokenize.py nella libreria standard adotta questo approccio "
Ben Creasy

38

Trovo questa spiegazione che cancella il mio dubbio. Perché c'è una possibilità che anche la persona che non sa Generatorsnon 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 functiondove avevamo interrotto. È qui che viene generatorsintrodotto il concetto di e l' yieldaffermazione riprende da dove era stata functioninterrotta.

  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 returne yieldin 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' yieldistruzione 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).


35

Esempio del mondo reale

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 domainse 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 yieldi risultati. Se hai usato la parola chiave returninvece 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()

sarebbe più pratico, se yield potesse essere spiegato in termini di programmazione ricorsiva / dianmica!
igaurav

27

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.


3
Se getBigChuckOfData non è pigro, allora non capisco quale vantaggio abbia il rendimento qui. Qual è un caso d'uso per questa funzione?
Sean Geoffrey Pietz

1
Ma il punto è che, IIUC, bufferedFetch sta rallentando la chiamata a getBigChunkOfData. Se getBigChunkOfData era già pigro, bufferedFetch sarebbe inutile. Ogni chiamata a bufferedFetch () restituirà un elemento buffer, anche se è già stato letto un BigChunk. E non è necessario tenere esplicitamente il conteggio dell'elemento successivo da restituire, perché i meccanismi di yield lo fanno implicitamente.
hmijail piange i dimessi il

22

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 forloop e ad altri costrutti di iterazione tradizionali.


19

La semplice spiegazione: considera una fordichiarazione

for item in iterable:
   do_stuff()

La maggior parte delle volte, iterablenon è 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

  • spazio (non è mai necessario memorizzare tutti gli elementi contemporaneamente) e
  • tempo (l'iterazione può terminare prima che tutti gli elementi siano necessari).

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.


... e una sequenza infinita potrebbe essere quella generata scorrendo ripetutamente un piccolo elenco, tornando all'inizio dopo aver raggiunto la fine. Lo uso per selezionare i colori nei grafici o per produrre pulsazioni o filatori impegnati nel testo.
Andrej Panjkov

@mataap: c'è un itertoolper quello - vedi cycles.
martineau

12

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.


8
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.
nosklo

reduceLocation è piuttosto strano produrre una lista, perché non produrre semplicemente ogni riga? Anche il filtro e la riduzione sono incorporati con comportamenti attesi (vedere la guida in ipython ecc.), L'uso di "reduce" è lo stesso del filtro.
James Antill

Buon punto sulle readlines (). Di solito mi rendo conto che i file sono iteratori di riga di prima classe durante i test di unità.
S.Lott

In realtà, la "riduzione" consiste nel combinare più linee singole in un oggetto composto. Va bene, è un elenco, ma è comunque una riduzione presa dalla fonte.
S.Lott

9

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_cornerse chiamarlo con for corner in myrect.iter_corners(). È solo più elegante da usare __iter__poiché possiamo usare il nome dell'istanza della classe direttamente fornell'espressione.


Adoravo l'idea di passare campi di classe simili come generatore
eusoubrasileiro

7

Fondamentalmente evitando le funzioni di richiamata durante l'iterazione sull'input mantenendo lo stato.

Vedi qui e qui per una panoramica di cosa si può fare usando i generatori.


4

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.


3

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 yieldda esplorazione, generatori per l'elaborazione parallela, sfuggire al limite di ricorsione, ecc.)

David Beazley sui generatori al PyCon 2014


2

Utilizzo i generatori quando il nostro server web funge da proxy:

  1. Il client richiede un URL proxy dal server
  2. Il server inizia a caricare l'URL di destinazione
  3. Il server cede per restituire i risultati al client non appena li ottiene

1

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.


if not any (candidate% prime for prime in primes_found) should be if all (candidate% prime for prime in primes_found)
rjmunro

Sì, volevo scrivere "se non qualsiasi (candidato% prime == 0 per primo in primes_found). Il tuo è leggermente più pulito, però. :)
Nick Johnson

Immagino che tu abbia dimenticato di eliminare il "non" da se non tutto (candidato% prime per primo in primes_found)
Thava

0

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