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.

È stato utile?

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

Evitando sostanzialmente le funzioni di richiamata durante l'iterazione sullo stato di mantenimento dell'input.

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

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.

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

David Beazley sui generatori al PyCon 2014

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)
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top