python generator yield - Cosa fa la parola chiave "rendimento"?



15 Answers

Collegamento al yield Grokking

Quando vedi una funzione con dichiarazioni di yield , applica questo semplice trucco per capire cosa accadrà:

  1. Inserisci un result = [] riga result = [] all'inizio della funzione.
  2. Sostituisci ogni yield expr con result.append(expr) .
  3. Inserisci un return result riga nella parte inferiore della funzione.
  4. Sì, niente più dichiarazioni di yield ! Leggi e calcola il codice.
  5. Confronta la funzione con la definizione originale.

Questo trucco può darti un'idea della logica alla base della funzione, ma ciò che effettivamente accade con il yield è significativamente diverso da ciò che accade nell'approccio basato sull'elenco. In molti casi, l'approccio di rendimento sarà molto più efficiente in termini di memoria e anche più veloce. In altri casi questo trucco ti farà rimanere bloccato in un ciclo infinito, anche se la funzione originale funziona perfettamente. Continuate a leggere per saperne di più...

Non confondere i tuoi Iterables, Iterators e Generators

Innanzitutto, il protocollo iteratore - quando scrivi

for x in mylist:
    ...loop body...

Python esegue i seguenti due passaggi:

  1. Ottiene un iteratore per mylist :

    Call iter(mylist) -> restituisce un oggetto con un metodo next() (o __next__() in Python 3).

    [Questo è il passo che molte persone dimenticano di dirti]

  2. Utilizza l'iteratore per eseguire il loop degli elementi:

    Continua a chiamare il metodo next() sull'iterator restituito dal passaggio 1. Il valore restituito da next() viene assegnato a x e il corpo del loop viene eseguito. Se viene sollevata un'eccezione StopIteration dall'interno next() , significa che non ci sono più valori nell'iteratore e il ciclo viene chiuso.

La verità è che Python esegue i suddetti due passaggi ogni volta che desidera eseguire il looping dei contenuti di un oggetto, quindi potrebbe essere un ciclo for, ma potrebbe anche essere un codice come otherlist.extend(mylist) (dove otherlist è un elenco Python) .

Qui mylist è iterabile perché implementa il protocollo iteratore. In una classe definita dall'utente, è possibile implementare il __iter__() per rendere iterabili le istanze della classe. Questo metodo dovrebbe restituire un iteratore . Un iteratore è un oggetto con un metodo next() . È possibile implementare sia __iter__() che next() sulla stessa classe e avere __iter__() return self . Ciò funzionerà per casi semplici, ma non quando si desidera che due iteratori eseguano il looping sullo stesso oggetto nello stesso momento.

Quindi questo è il protocollo iteratore, molti oggetti implementano questo protocollo:

  1. Elenchi, dizionari, tuple, insiemi, file incorporati.
  2. Classi definite dall'utente che implementano __iter__() .
  3. Generatori.

Si noti che un ciclo for non conosce il tipo di oggetto con cui si sta occupando - segue solo il protocollo iteratore ed è felice di ottenere item after item come chiama next() . Gli elenchi integrati restituiscono i loro articoli uno per uno, i dizionari restituiscono le chiavi una alla volta, i file restituiscono le righe una per una, ecc. E i generatori restituiscono ... beh, è ​​qui che arriva la yield :

def f123():
    yield 1
    yield 2
    yield 3

for item in f123():
    print item

Invece di dichiarazioni di yield , se avessi tre istruzioni di return in f123() solo il primo verrebbe eseguito e la funzione f123() . Ma f123() non è una funzione ordinaria. Quando viene chiamato f123() , non restituisce alcun valore nelle dichiarazioni di rendimento! Restituisce un oggetto generatore. Inoltre, la funzione non esce realmente - entra in uno stato sospeso. Quando il ciclo for tenta di eseguire il loop sull'oggetto generatore, la funzione riprende dallo stato sospeso alla riga successiva dopo il yield è ritornata in precedenza, esegue la riga successiva del codice, in questo caso un'istruzione yield e la restituisce come il prossimo oggetto. Questo accade fino a quando la funzione non si chiude, a quel punto il generatore solleva StopIteration e il loop termina.

Quindi l'oggetto generatore è un po 'come un adattatore - ad una estremità mostra il protocollo iteratore, esponendo i __iter__() e next() per mantenere felice il ciclo for . All'altro capo, tuttavia, esegue la funzione quel tanto che basta per ricavarne il valore successivo e la rimette in modalità sospesa.

Perché usare i generatori?

Di solito è possibile scrivere codice che non utilizza generatori ma implementa la stessa logica. Un'opzione è usare il "trucco" di elenco temporaneo che ho menzionato prima. Ciò non funzionerà in tutti i casi, ad esempio se si hanno cicli infiniti o se si fa un uso inefficiente della memoria quando si ha una lista molto lunga. L'altro approccio consiste nell'implementare una nuova classe iterabile SomethingIter che mantiene lo stato nei membri di istanza ed esegue il prossimo passo logico nel suo metodo next() (o __next__() in Python 3). A seconda della logica, il codice all'interno del metodo next() può sembrare molto complesso e soggetto a bug. Qui i generatori forniscono una soluzione semplice e pulita.

list build methods

Qual è l'uso della parola chiave yield in Python? Che cosa fa?

Ad esempio, sto cercando di capire questo codice 1 :

def _get_child_candidates(self, distance, min_dist, max_dist):
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild  

E questo è il chiamante:

result, candidates = [], [self]
while candidates:
    node = candidates.pop()
    distance = node._get_dist(obj)
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result

Cosa succede quando viene chiamato il metodo _get_child_candidates ? È stata restituita una lista? Un singolo elemento? Si chiama di nuovo? Quando si fermeranno le chiamate successive?

1. Il codice proviene da Jochen Schulz (jrschulz), che ha creato una grande libreria Python per gli spazi metrici. Questo è il link alla fonte completa: Modulo mspace .




La parola chiave yield è ridotta a due semplici fatti:

  1. Se il compilatore rileva la parola chiave yield ovunque all'interno di una funzione, quella funzione non ritorna più attraverso l'istruzione return . Invece , restituisce immediatamente un pigro "elenco in attesa" oggetto chiamato generatore
  2. Un generatore è iterabile. Cos'è un iterable ? È un po 'come una list o un set o un range o una vista dict, con un protocollo integrato per visitare ogni elemento in un certo ordine .

In breve: un generatore è un elenco pigro, in pendenza incrementale , e le dichiarazioni di yield consentono di utilizzare la notazione della funzione per programmare i valori di lista che il generatore deve sputare in modo incrementale.

generator = myYieldingFunction(...)
x = list(generator)

   generator
       v
[x[0], ..., ???]

         generator
             v
[x[0], x[1], ..., ???]

               generator
                   v
[x[0], x[1], x[2], ..., ???]

                       StopIteration exception
[x[0], x[1], x[2]]     done

list==[x[0], x[1], x[2]]

Esempio

Definiamo una funzione makeRange che è proprio come la range di Python. Chiamando makeRange(n) RESTITUISCE UN GENERATORE:

def makeRange(n):
    # return 0,1,2,...,n-1
    i = 0
    while i < n:
        yield i
        i += 1

>>> makeRange(5)
<generator object makeRange at 0x19e4aa0>

Per forzare il generatore a restituire immediatamente i suoi valori in sospeso, puoi passarlo in list() (proprio come qualsiasi altro iterabile):

>>> list(makeRange(5))
[0, 1, 2, 3, 4]

Confronto di esempio con "solo restituendo una lista"

L'esempio sopra può essere pensato semplicemente come la creazione di una lista a cui si aggiunge e si restituisce:

# list-version                   #  # generator-version
def makeRange(n):                #  def makeRange(n):
    """return [0,1,2,...,n-1]""" #~     """return 0,1,2,...,n-1"""
    TO_RETURN = []               #>
    i = 0                        #      i = 0
    while i < n:                 #      while i < n:
        TO_RETURN += [i]         #~         yield i
        i += 1                   #          i += 1  ## indented
    return TO_RETURN             #>

>>> makeRange(5)
[0, 1, 2, 3, 4]

C'è una grande differenza, però; guarda l'ultima sezione.

Come puoi usare i generatori

Un iterable è l'ultima parte di una list comprehension, e tutti i generatori sono iterabili, quindi vengono spesso utilizzati in questo modo:

#                   _ITERABLE_
>>> [x+10 for x in makeRange(5)]
[10, 11, 12, 13, 14]

Per avere una sensazione migliore per i generatori, puoi giocare con il modulo itertools (assicurati di usare chain.from_iterable piuttosto che chain quando richiesto). Ad esempio, potresti persino utilizzare i generatori per implementare elenchi pigri infinitamente lunghi come itertools.count() . È possibile implementare il proprio def enumerate(iterable): zip(count(), iterable) o in alternativa farlo con la parola chiave yield in un ciclo while.

Nota: i generatori possono essere effettivamente utilizzati per molte altre cose, come l' implementazione di coroutine o programmazione non deterministica o altre cose eleganti. Tuttavia, il punto di vista delle "liste pigre" che presento qui è l'uso più comune che troverete.

Dietro le quinte

Questo è il modo in cui funziona il "protocollo di iterazione Python". Cioè, cosa sta succedendo quando fai una list(makeRange(5)) . Questo è ciò che descrivo in precedenza come un "elenco pigro e incrementale".

>>> x=iter(range(5))
>>> next(x)
0
>>> next(x)
1
>>> next(x)
2
>>> next(x)
3
>>> next(x)
4
>>> next(x)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

La funzione built-in next() chiama semplicemente la funzione objects .next() , che è una parte del "protocollo di iterazione" e si trova su tutti gli iteratori. Puoi usare manualmente la funzione next() (e altre parti del protocollo di iterazione) per implementare cose di fantasia, di solito a scapito della leggibilità, quindi cerca di evitare di farlo ...

minutiae

Normalmente, la maggior parte delle persone non si preoccuperebbe delle seguenti distinzioni e probabilmente vorrebbe smettere di leggere qui.

In Python-speak, un iterable è qualsiasi oggetto che "capisca il concetto di un ciclo for" come una lista [1,2,3] , e un iteratore è un'istanza specifica del ciclo for richiesto come [1,2,3].__iter__() . Un generatore è esattamente uguale a qualsiasi iteratore, tranne per il modo in cui è stato scritto (con la sintassi della funzione).

Quando si richiede un iteratore da un elenco, viene creato un nuovo iteratore. Tuttavia, quando richiedi un iteratore da un iteratore (cosa che raramente faresti), ti dà solo una copia di se stesso.

Quindi, nell'improbabile caso che tu non riesca a fare qualcosa del genere ...

> x = myRange(5)
> list(x)
[0, 1, 2, 3, 4]
> list(x)
[]

... quindi ricorda che un generatore è un iteratore ; cioè, è una tantum. Se vuoi riutilizzarlo, devi chiamare myRange(...) nuovo. Se è necessario utilizzare il risultato due volte, convertire il risultato in un elenco e memorizzarlo in una variabile x = list(myRange(5)) . Coloro che hanno assolutamente bisogno di clonare un generatore (ad esempio, che stanno facendo terrificante metaprogrammazione hacker) possono usare itertools.tee se assolutamente necessario, dal momento che la proposta di standard Python PEP iteratori di copia è stata posticipata.




yieldè proprio come return- restituisce qualsiasi cosa tu gli dica (come un generatore). La differenza è che la prossima volta che chiamate il generatore, l'esecuzione inizia dall'ultima chiamata yieldall'istruzione. A differenza del ritorno, il frame dello stack non viene ripulito quando si verifica una resa, tuttavia il controllo viene trasferito al chiamante, quindi il suo stato riprenderà la volta successiva la funzione.

Nel caso del tuo codice, la funzione get_child_candidatesagisce come un iteratore in modo che quando estendi la tua lista, aggiunge un elemento alla volta al nuovo elenco.

list.extendchiama un iteratore finché non è esaurito. Nel caso dell'esempio di codice che hai postato, sarebbe molto più semplice restituire una tupla e aggiungerla all'elenco.




Per coloro che preferiscono un esempio di lavoro minimo, meditate su questa sessione interattiva di Python :

>>> def f():
...   yield 1
...   yield 2
...   yield 3
... 
>>> g = f()
>>> for i in g:
...   print i
... 
1
2
3
>>> for i in g:
...   print i
... 
>>> # Note that this time nothing was printed



TL; DR

Invece di questo:

def squares_list(n):
    the_list = []                         # Replace
    for x in range(n):
        y = x * x
        the_list.append(y)                # these
    return the_list                       # lines

Fai questo:

def squares_the_yield_way(n):
    for x in range(n):
        y = x * x
        yield y                           # with this one.

Ogni volta che ti ritrovi a costruire una lista da zero, yieldogni pezzo invece.

Questo è stato il mio primo momento "aha" con rendimento.

yield è un modo zuccheroso per dire

costruisci una serie di cose

Stesso comportamento:

>>> for square in squares_list(4):
...     print(square)
...
0
1
4
9
>>> for square in squares_the_yield_way(4):
...     print(square)
...
0
1
4
9

Comportamento diverso:

Il rendimento è single-pass : puoi solo ripetere una volta. Quando una funzione ha un rendimento, la chiamiamo funzione generatore . E un iterator è ciò che restituisce. Questo è rivelatore. Perdiamo la comodità di un contenitore, ma otteniamo il potere di una serie arbitrariamente lunga.

Il rendimento è pigro , mette fuori calcolo. Una funzione con un rendimento in esso in realtà non viene eseguita affatto quando la chiami. L'oggetto iteratore che restituisce utilizza la magic per mantenere il contesto interno della funzione. Ogni volta che chiamate next()l'iteratore (questo accade in un ciclo for), i pollici dell'esecuzione avanzano al rendimento successivo. ( returnsolleva StopIteratione termina la serie.)

Il rendimento è versatile . Può fare loop infiniti:

>>> def squares_all_of_them():
...     x = 0
...     while True:
...         yield x * x
...         x += 1
...
>>> squares = squares_all_of_them()
>>> for _ in range(4):
...     print(next(squares))
...
0
1
4
9

Se hai bisogno di più pass e la serie non è troppo lunga, basta chiamarci list():

>>> list(squares_the_yield_way(4))
[0, 1, 4, 9]

Scelta geniale della parola yieldperché si applicano entrambi i significati :

rendimento - produrre o fornire (come in agricoltura)

... fornire i prossimi dati della serie.

rendimento - cedere o rinunciare (come nel potere politico)

... rinuncia all'esecuzione della CPU fino all'avanzamento dell'iteratore.




C'è un tipo di risposta che non mi sento ancora stato dato, tra le tante grandi risposte che descrivono come usare i generatori. Ecco la risposta della teoria del linguaggio di programmazione:

L' yieldistruzione in Python restituisce un generatore. Un generatore in Python è una funzione che restituisce continuazioni (e in particolare un tipo di coroutine, ma le continuazioni rappresentano il meccanismo più generale per capire cosa sta succedendo).

Le continuazioni nella teoria dei linguaggi di programmazione sono un tipo di calcolo molto più fondamentale, ma non sono spesso utilizzate, perché sono estremamente difficili da ragionare e anche molto difficili da implementare. Ma l'idea di cosa sia una continuazione, è semplice: è lo stato di una computazione che non è ancora finita. In questo stato, vengono salvati i valori correnti delle variabili, le operazioni che devono ancora essere eseguite e così via. Quindi, in un momento successivo del programma, è possibile richiamare la continuazione, in modo tale che le variabili del programma vengano reimpostate su tale stato e vengano eseguite le operazioni che sono state salvate.

Le continue, in questa forma più generale, possono essere implementate in due modi. Nel call/ccmodo, lo stack del programma viene letteralmente salvato e quindi quando viene richiamata la continuazione, lo stack viene ripristinato.

In continuation passing style (CPS), le continuazioni sono solo funzioni normali (solo nelle lingue in cui le funzioni sono di prima classe) che il programmatore gestisce e passa esplicitamente alle subroutine. In questo stile, lo stato del programma è rappresentato dalle chiusure (e dalle variabili che sono codificate in esse) piuttosto che dalle variabili che risiedono in un punto dello stack. Le funzioni che gestiscono il flusso di controllo accettano la continuazione come argomenti (in alcune varianti di CPS, le funzioni possono accettare più continuazioni) e manipolano il flusso di controllo invocandole semplicemente chiamandole e ritornando in seguito. Un esempio molto semplice di continuation passing style è il seguente:

def save_file(filename):
  def write_file_continuation():
    write_stuff_to_file(filename)

  check_if_file_exists_and_user_wants_to_overwrite(write_file_continuation)

In questo esempio (molto semplicistico), il programmatore salva l'operazione di scrivere effettivamente il file in una continuazione (che può essere potenzialmente un'operazione molto complessa con molti dettagli da scrivere), e quindi passa quella continuazione (cioè, come prima chiusura di classe) a un altro operatore che esegue un po 'più di elaborazione, e poi lo chiama se necessario. (Uso questo schema di progettazione molto nella programmazione GUI effettiva, sia perché mi salva le righe di codice o, soprattutto, per gestire il flusso di controllo dopo l'attivazione degli eventi della GUI.)

Il resto di questo post, senza perdita di generalità, concettualizzerà le continuazioni come CPS, perché è molto più facile da capire e leggere.


Ora parliamo di generatori in Python. I generatori sono uno specifico sottotipo di continuazione. Mentre le continuazioni sono in generale in grado di salvare lo stato di un calcolo (cioè lo stack di chiamate del programma), i generatori sono in grado di salvare lo stato di iterazione solo su un iteratore . Anche se, questa definizione è leggermente fuorviante per alcuni casi d'uso di generatori. Per esempio:

def f():
  while True:
    yield 4

Questo è chiaramente un iterabile ragionevole il cui comportamento è ben definito - ogni volta che il generatore lo itera sopra, restituisce 4 (e lo fa per sempre). Ma non è probabilmente il tipo prototipo di iterabile che viene in mente quando si pensa agli iteratori (cioè, for x in collection: do_something(x)). Questo esempio illustra il potere dei generatori: se qualcosa è un iteratore, un generatore può salvare lo stato della sua iterazione.

Per ripetere: le continue possono salvare lo stato dello stack di un programma e i generatori possono salvare lo stato di iterazione. Ciò significa che le continuazioni sono molto più potenti dei generatori, ma anche che i generatori sono molto, molto più semplici. Sono più facili da implementare per il progettista di linguaggi e sono più facili da usare per il programmatore (se hai tempo per masterizzare, prova a leggere e comprendere questa pagina sui continuations e call / cc ).

Ma potresti facilmente implementare (e concettualizzare) i generatori come un caso semplice e specifico di stile di passaggio continuo:

Ogni volta che yieldviene chiamato, dice alla funzione di restituire una continuazione. Quando la funzione viene richiamata, parte da dove era stata interrotta. Quindi, in pseudo-pseudocodice (cioè, non pseudocodice, ma non codice) il nextmetodo del generatore è fondamentalmente il seguente:

class Generator():
  def __init__(self,iterable,generatorfun):
    self.next_continuation = lambda:generatorfun(iterable)

  def next(self):
    value, next_continuation = self.next_continuation()
    self.next_continuation = next_continuation
    return value

dove la yieldparola chiave è in realtà zucchero sintattico per la funzione generatore reale, in pratica qualcosa come:

def generatorfun(iterable):
  if len(iterable) == 0:
    raise StopIteration
  else:
    return (iterable[0], lambda:generatorfun(iterable[1:]))

Ricorda che questo è solo uno pseudocodice e l'effettiva implementazione dei generatori in Python è più complessa. Ma come esercizio per capire cosa sta succedendo, prova a utilizzare lo stile di passaggio continuo per implementare oggetti generatori senza utilizzare la yieldparola chiave.




Mentre molte risposte mostrano il motivo per cui dovresti usare a yieldper creare un generatore, ci sono più usi per yield. È abbastanza facile creare una coroutine, che consente il passaggio di informazioni tra due blocchi di codice. Non ripeterò nessuno dei begli esempi che sono già stati dati sull'uso yieldper creare un generatore.

Per aiutare a capire cosa yieldfa nel codice seguente, puoi usare il dito per tracciare il ciclo attraverso qualsiasi codice che abbia un yield. Ogni volta che il dito colpisce il yield, bisogna aspettare per una nexto sendda inserire. Quando nextviene chiamato un, si traccia attraverso il codice fino a quando non si preme il yield... il codice a destra di yieldviene valutato e restituito al chiamante ... quindi si attende. Quando nextviene richiamato, si esegue un altro ciclo attraverso il codice. Tuttavia, noterete che in una coroutine, yieldpuò anche essere usato con un send... che invierà un valore dal chiamante nella funzione di resa. Se a sendviene dato, allorayieldriceve il valore inviato e lo sputa dal lato sinistro ... quindi la traccia attraverso il codice progredisce fino a quando non si preme di yieldnuovo (restituendo il valore alla fine, come se nextfosse stato chiamato).

Per esempio:

>>> def coroutine():
...     i = -1
...     while True:
...         i += 1
...         val = (yield i)
...         print("Received %s" % val)
...
>>> sequence = coroutine()
>>> sequence.next()
0
>>> sequence.next()
Received None
1
>>> sequence.send('hello')
Received hello
2
>>> sequence.close()



Stavo per postare "leggi la pagina 19 di" Python: Essential Reference "di Beazley per una rapida descrizione dei generatori", ma molti altri hanno già pubblicato buone descrizioni.

Inoltre, si noti che yieldpuò essere usato nelle coroutine come il doppio del loro uso nelle funzioni del generatore. Sebbene non sia lo stesso uso dello snippet di codice, (yield)può essere usato come espressione in una funzione. Quando un chiamante invia un valore al metodo utilizzando il send()metodo, la coroutine verrà eseguita fino a quando (yield)non viene rilevata la successiva istruzione.

I generatori e le coroutine sono un ottimo modo per configurare applicazioni di tipo flusso di dati. Ho pensato che valesse la pena conoscere l'altro uso yielddell'affermazione delle funzioni.




Da un punto di vista della programmazione, gli iteratori sono implementati come thunks .

Per implementare iteratori, generatori e pool di thread per l'esecuzione simultanea, ecc. Come thunk (chiamati anche funzioni anonime), si utilizzano i messaggi inviati a un oggetto di chiusura, che ha un dispatcher, e il dispatcher risponde ai "messaggi".

http://en.wikipedia.org/wiki/Message_passing

" next " è un messaggio inviato a una chiusura, creato dalla chiamata " iter ".

Ci sono molti modi per implementare questo calcolo. Ho usato la mutazione, ma è facile farlo senza mutazione, restituendo il valore corrente e il prossimo yielder.

Ecco una dimostrazione che utilizza la struttura di R6RS, ma la semantica è assolutamente identica a quella di Python. È lo stesso modello di computazione, e solo una modifica della sintassi è necessaria per riscriverlo in Python.

Welcome to Racket v6.5.0.3.

-> (define gen
     (lambda (l)
       (define yield
         (lambda ()
           (if (null? l)
               'END
               (let ((v (car l)))
                 (set! l (cdr l))
                 v))))
       (lambda(m)
         (case m
           ('yield (yield))
           ('init  (lambda (data)
                     (set! l data)
                     'OK))))))
-> (define stream (gen '(1 2 3)))
-> (stream 'yield)
1
-> (stream 'yield)
2
-> (stream 'yield)
3
-> (stream 'yield)
'END
-> ((stream 'init) '(a b))
'OK
-> (stream 'yield)
'a
-> (stream 'yield)
'b
-> (stream 'yield)
'END
-> (stream 'yield)
'END
->



Qui c'è un semplice esempio:

def isPrimeNumber(n):
    print "isPrimeNumber({}) call".format(n)
    if n==1:
        return False
    for x in range(2,n):
        if n % x == 0:
            return False
    return True

def primes (n=1):
    while(True):
        print "loop step ---------------- {}".format(n)
        if isPrimeNumber(n): yield n
        n += 1

for n in primes():
    if n> 10:break
    print "wiriting result {}".format(n)

Produzione:

loop step ---------------- 1
isPrimeNumber(1) call
loop step ---------------- 2
isPrimeNumber(2) call
loop step ---------------- 3
isPrimeNumber(3) call
wiriting result 3
loop step ---------------- 4
isPrimeNumber(4) call
loop step ---------------- 5
isPrimeNumber(5) call
wiriting result 5
loop step ---------------- 6
isPrimeNumber(6) call
loop step ---------------- 7
isPrimeNumber(7) call
wiriting result 7
loop step ---------------- 8
isPrimeNumber(8) call
loop step ---------------- 9
isPrimeNumber(9) call
loop step ---------------- 10
isPrimeNumber(10) call
loop step ---------------- 11
isPrimeNumber(11) call

Non sono uno sviluppatore Python, ma mi sembra che yieldmantenga la posizione del flusso del programma e il prossimo ciclo inizi dalla posizione "yield". Sembra che stia aspettando quella posizione, e poco prima, restituendo un valore all'esterno, e la prossima volta continui a lavorare.

Sembra essere un'abilità interessante e piacevole: D




Come ogni risposta suggerisce, yieldè usata per creare un generatore di sequenze. È usato per generare alcune sequenze in modo dinamico. Ad esempio, durante la lettura di un file riga per riga su una rete, è possibile utilizzare la yieldfunzione come segue:

def getNextLines():
   while con.isOpen():
       yield con.read()

Puoi usarlo nel tuo codice come segue:

for line in getNextLines():
    doSomeThing(line)

Gotcha di controllo dell'esecuzione

Il controllo di esecuzione verrà trasferito da getNextLines () al forciclo quando viene eseguito il rendimento. Pertanto, ogni volta che viene richiamato getNextLines (), l'esecuzione inizia dal punto in cui è stata messa in pausa l'ultima volta.

Quindi, in breve, una funzione con il seguente codice

def simpleYield():
    yield "first time"
    yield "second time"
    yield "third time"
    yield "Now some useful value {}".format(12)

for i in simpleYield():
    print i

stamperà

"first time"
"second time"
"third time"
"Now some useful value 12"



In breve, l' yieldistruzione trasforma la tua funzione in una fabbrica che produce un oggetto speciale chiamato a generatorche avvolge il corpo della tua funzione originale. Quando generatorviene iterato, esegue la funzione finché non raggiunge il successivo, yieldquindi sospende l'esecuzione e valuta il valore passato a yield. Ripete questo processo su ogni iterazione finché il percorso di esecuzione non esce dalla funzione. Per esempio,

def simple_generator():
    yield 'one'
    yield 'two'
    yield 'three'

for i in simple_generator():
    print i

semplicemente uscite

one
two
three

Il potere deriva dall'usare il generatore con un loop che calcola una sequenza, il generatore esegue il ciclo arrestandosi ogni volta per "cedere" al risultato successivo del calcolo, in questo modo calcola una lista al volo, il vantaggio è la memoria salvato per calcoli particolarmente grandi

Supponiamo che tu voglia creare una tua rangefunzione che produca una gamma iterabile di numeri, potresti farlo in questo modo,

def myRangeNaive(i):
    n = 0
    range = []
    while n < i:
        range.append(n)
        n = n + 1
    return range

e usarlo in questo modo;

for i in myRangeNaive(10):
    print i

Ma questo è inefficiente perché

  • Crei un array che usi una sola volta (questo spreca memoria)
  • Questo codice scorre in realtà su quell'array due volte! :(

Fortunatamente Guido e il suo team sono stati abbastanza generosi da sviluppare generatori in modo da poterlo fare;

def myRangeSmart(i):
    n = 0
    while n < i:
       yield n
       n = n + 1
    return

for i in myRangeSmart(10):
    print i

Ora su ogni iterazione una funzione sul generatore chiamato next()esegue la funzione fino a quando non raggiunge un'istruzione 'yield' in cui si arresta e 'produce' il valore o raggiunge la fine della funzione. In questo caso alla prima chiamata, next()esegue la dichiarazione di rendimento e restituisce 'n', alla prossima chiamata eseguirà l'istruzione incrementale, tornerà al 'while', la valuterà e, se è vera, si fermerà e restituisce 'n' di nuovo, continuerà in questo modo finché la condizione while non ritorna falsa e il generatore salta alla fine della funzione.




(La mia risposta qui sotto parla solo dal punto di vista dell'uso del generatore Python, non dell'implementazione sottostante del meccanismo generatore , che implica alcuni trucchi di manipolazione dello stack e dell'heap.)

Quando yieldviene usato al posto di a returnin una funzione python, quella funzione viene trasformata in qualcosa di speciale chiamato generator function. Quella funzione restituirà un oggetto di generatortipo. La yieldparola chiave è un flag per notificare al compilatore Python il trattamento di tale funzione in modo speciale. Le normali funzioni terminano quando viene restituito un valore da esso. Ma con l'aiuto del compilatore, la funzione del generatore può essere pensata come riassumibile. Cioè, il contesto di esecuzione verrà ripristinato e l'esecuzione continuerà dall'ultima esecuzione. Fino a quando non si richiama esplicitamente return, che genererà StopIterationun'eccezione (che è anche parte del protocollo iteratore) o raggiungerà la fine della funzione. Ho trovato un sacco di riferimenti circa generator, ma questo onedal functional programming perspectiveè il più digeribile.

(Ora voglio parlare della logica alla base generator, e iteratorbasata sulla mia stessa comprensione. Spero che questo possa aiutarti a cogliere la motivazione essenziale di iteratore e generatore.Questo concetto si presenta anche in altre lingue come C #.)

Come capisco, quando vogliamo elaborare una serie di dati, di solito prima salviamo i dati da qualche parte e poi li elaboriamo uno per uno. Ma questo approccio intuitivo è problematico. Se il volume dei dati è enorme, è costoso archiviarli nel loro insieme in anticipo. Quindi, invece di memorizzare datadirettamente la stessa, perché non memorizzare una sorta di metadataindirettamente, vale a direthe logic how the data is computed .

Esistono 2 approcci per avvolgere tali metadati.

  1. L'approccio OO, avvolgiamo i metadati as a class. Questo è il cosiddetto iteratorche implementa il protocollo iteratore (cioè i metodi __next__()e __iter__()). Questo è anche il modello di disegno iteratore comunemente visto .
  2. L'approccio funzionale, avvolgiamo i metadati as a function. Questo è il cosiddetto generator function. Ma sotto la cappa, l' iteratore generator objectancora restituito IS-Aperché implementa anche il protocollo iteratore.

In entrambi i casi, viene creato un iteratore, ovvero un oggetto che può darti i dati che desideri. L'approccio OO potrebbe essere un po 'complesso. Ad ogni modo, quale usare dipende da te.




La yieldparola chiave raccoglie semplicemente i risultati di ritorno. Pensa a yieldcomereturn +=




Ancora un altro TL; DR

Iterator on list : next()restituisce il prossimo elemento della lista

Generatore di Iterator : next()calcolerà il prossimo elemento al volo (esegui il codice)

Puoi vedere il rendimento / generatore come un modo per eseguire manualmente il flusso di controllo dall'esterno (come il ciclo continuo di un passo), chiamando next, per quanto complesso il flusso.

Nota : il generatore NON è una funzione normale. Ricorda lo stato precedente come le variabili locali (stack). Vedi altre risposte o articoli per una spiegazione dettagliata. Il generatore può essere iterato solo una volta . Potresti fare a meno yield, ma non sarebbe così bello, quindi può essere considerato zucchero di lingua "molto carino".




Related