Che cosa fa la parola chiave "yield"?


10199

Qual è l'uso di yield parola chiave in Python e 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 _get_child_candidatesviene chiamato il metodo ? Viene restituito un elenco? Un singolo elemento? Si chiama di nuovo? Quando si interromperanno le chiamate successive?


1. Questo pezzo di codice è stato scritto da Jochen Schulz (jrschulz), che ha realizzato una grande libreria Python per spazi metrici. Questo è il collegamento alla fonte completa: Modulo mspace .

Risposte:


14652

Per capire cosa yieldfa, devi capire cosa sono i generatori . E prima che tu possa capire i generatori, devi capire gli iterabili .

iterabili

Quando si crea un elenco, è possibile leggere i suoi elementi uno per uno. Leggere i suoi elementi uno per uno si chiama iterazione:

>>> mylist = [1, 2, 3]
>>> for i in mylist:
...    print(i)
1
2
3

mylistè un iterabile . Quando si utilizza la comprensione di un elenco, si crea un elenco e quindi un iterabile:

>>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
...    print(i)
0
1
4

Tutto ciò che puoi usare " for... in..." su è iterabile; lists, strings, File ...

Questi iterabili sono utili perché puoi leggerli quanto vuoi, ma memorizzi tutti i valori in memoria e questo non è sempre quello che vuoi quando hai molti valori.

generatori

I generatori sono iteratori, un tipo di iterabile che puoi ripetere una sola volta . I generatori non memorizzano tutti i valori in memoria, generano i valori al volo :

>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
...    print(i)
0
1
4

È lo stesso, tranne che hai usato al ()posto di []. MA non puoi esibirtifor i in mygenerator una seconda volta poiché i generatori possono essere usati solo una volta: calcolano 0, poi si dimenticano di esso e calcolano 1, e terminano il calcolo 4, uno per uno.

dare la precedenza

yieldè una parola chiave che viene utilizzata come return, tranne per il fatto che la funzione restituirà un generatore.

>>> def createGenerator():
...    mylist = range(3)
...    for i in mylist:
...        yield i*i
...
>>> mygenerator = createGenerator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object createGenerator at 0xb7555c34>
>>> for i in mygenerator:
...     print(i)
0
1
4

Qui è un esempio inutile, ma è utile quando sai che la tua funzione restituirà un enorme set di valori che dovrai leggere solo una volta.

Per padroneggiare yield, devi capire che quando chiami la funzione, il codice che hai scritto nel corpo della funzione non viene eseguito. La funzione restituisce solo l'oggetto generatore, questo è un po 'complicato :-)

Quindi, il codice continuerà da dove era stato interrotto ogni volta che forutilizza il generatore.

Ora la parte difficile:

La prima volta che forchiama l'oggetto generatore creato dalla tua funzione, eseguirà il codice nella tua funzione dall'inizio fino a quando non colpisce yield, quindi restituirà il primo valore del ciclo. Quindi, ogni chiamata successiva eseguirà un'altra iterazione del ciclo che hai scritto nella funzione e restituirà il valore successivo. Ciò continuerà fino a quando il generatore viene considerato vuoto, cosa che accade quando la funzione viene eseguita senza colpire yield. Ciò può essere dovuto al fatto che il ciclo è terminato o perché non si soddisfa più un "if/else".


Il tuo codice ha spiegato

Generatore:

# Here you create the method of the node object that will return the generator
def _get_child_candidates(self, distance, min_dist, max_dist):

    # Here is the code that will be called each time you use the generator object:

    # If there is still a child of the node object on its left
    # AND if the distance is ok, return the next child
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild

    # If there is still a child of the node object on its right
    # AND if the distance is ok, return the next child
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild

    # If the function arrives here, the generator will be considered empty
    # there is no more than two values: the left and the right children

Caller:

# Create an empty list and a list with the current object reference
result, candidates = list(), [self]

# Loop on candidates (they contain only one element at the beginning)
while candidates:

    # Get the last candidate and remove it from the list
    node = candidates.pop()

    # Get the distance between obj and the candidate
    distance = node._get_dist(obj)

    # If distance is ok, then you can fill the result
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)

    # Add the children of the candidate in the candidate's list
    # so the loop will keep running until it will have looked
    # at all the children of the children of the children, etc. of the candidate
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))

return result

Questo codice contiene diverse parti intelligenti:

  • Il ciclo scorre su un elenco, ma l'elenco si espande mentre il ciclo viene ripetuto :-) È un modo conciso di esaminare tutti questi dati nidificati anche se è un po 'pericoloso poiché puoi finire con un ciclo infinito. In questo caso, candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))esaurisci tutti i valori del generatore, ma whilecontinua a creare nuovi oggetti generatore che produrranno valori diversi dai precedenti poiché non sono applicati sullo stesso nodo.

  • Il extend()metodo è un metodo di oggetto elenco che prevede un iterabile e aggiunge i suoi valori all'elenco.

Di solito passiamo un elenco ad esso:

>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]

Ma nel tuo codice, ottiene un generatore, che è buono perché:

  1. Non è necessario leggere i valori due volte.
  2. Potresti avere molti figli e non li vuoi tutti memorizzati.

E funziona perché a Python non importa se l'argomento di un metodo è un elenco o meno. Python si aspetta iterabili, quindi funzionerà con stringhe, elenchi, tuple e generatori! Questo si chiama Duck Typing ed è uno dei motivi per cui Python è così bello. Ma questa è un'altra storia, per un'altra domanda ...

Puoi fermarti qui o leggere un po 'per vedere un uso avanzato di un generatore:

Controllo dell'esaurimento del generatore

>>> class Bank(): # Let's create a bank, building ATMs
...    crisis = False
...    def create_atm(self):
...        while not self.crisis:
...            yield "$100"
>>> hsbc = Bank() # When everything's ok the ATM gives you as much as you want
>>> corner_street_atm = hsbc.create_atm()
>>> print(corner_street_atm.next())
$100
>>> print(corner_street_atm.next())
$100
>>> print([corner_street_atm.next() for cash in range(5)])
['$100', '$100', '$100', '$100', '$100']
>>> hsbc.crisis = True # Crisis is coming, no more money!
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> wall_street_atm = hsbc.create_atm() # It's even true for new ATMs
>>> print(wall_street_atm.next())
<type 'exceptions.StopIteration'>
>>> hsbc.crisis = False # The trouble is, even post-crisis the ATM remains empty
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> brand_new_atm = hsbc.create_atm() # Build a new one to get back in business
>>> for cash in brand_new_atm:
...    print cash
$100
$100
$100
$100
$100
$100
$100
$100
$100
...

Nota: per Python 3, utilizzare print(corner_street_atm.__next__())oprint(next(corner_street_atm))

Può essere utile per varie cose come il controllo dell'accesso a una risorsa.

Itertools, il tuo migliore amico

Il modulo itertools contiene funzioni speciali per manipolare gli iterabili. Hai mai desiderato duplicare un generatore? Catena di due generatori? Raggruppare i valori in un elenco nidificato con una riga? Map / Zipsenza creare un altro elenco?

Quindi basta import itertools.

Un esempio? Vediamo i possibili ordini di arrivo per una corsa a quattro cavalli:

>>> horses = [1, 2, 3, 4]
>>> races = itertools.permutations(horses)
>>> print(races)
<itertools.permutations object at 0xb754f1dc>
>>> print(list(itertools.permutations(horses)))
[(1, 2, 3, 4),
 (1, 2, 4, 3),
 (1, 3, 2, 4),
 (1, 3, 4, 2),
 (1, 4, 2, 3),
 (1, 4, 3, 2),
 (2, 1, 3, 4),
 (2, 1, 4, 3),
 (2, 3, 1, 4),
 (2, 3, 4, 1),
 (2, 4, 1, 3),
 (2, 4, 3, 1),
 (3, 1, 2, 4),
 (3, 1, 4, 2),
 (3, 2, 1, 4),
 (3, 2, 4, 1),
 (3, 4, 1, 2),
 (3, 4, 2, 1),
 (4, 1, 2, 3),
 (4, 1, 3, 2),
 (4, 2, 1, 3),
 (4, 2, 3, 1),
 (4, 3, 1, 2),
 (4, 3, 2, 1)]

Comprensione dei meccanismi interni dell'iterazione

L'iterazione è un processo che implica iterabili (implementazione del __iter__()metodo) e iteratori (implementazione del __next__()metodo). Gli Iterabili sono tutti gli oggetti da cui è possibile ottenere un iteratore. Gli iteratori sono oggetti che ti consentono di iterare su iterabili.

C'è di più in questo articolo su come forfunzionano i loop .


355
yieldnon è magico come suggerisce questa risposta. Quando si chiama una funzione che contiene yieldun'istruzione ovunque, si ottiene un oggetto generatore, ma non viene eseguito alcun codice. Quindi ogni volta che si estrae un oggetto dal generatore, Python esegue il codice nella funzione fino a quando non arriva a yieldun'istruzione, quindi mette in pausa e consegna l'oggetto. Quando si estrae un altro oggetto, Python riprende subito dopo yielde continua fino a quando non raggiunge un altro yield(spesso lo stesso, ma una iterazione in seguito). Questo continua fino a quando la funzione termina alla fine, a quel punto il generatore viene considerato esaurito.
Matthias Fripp,

30
"Questi iterabili sono utili ... ma conservi tutti i valori in memoria e questo non è sempre quello che vuoi", è sbagliato o confuso. Un iterabile restituisce un iteratore quando chiama iter () sull'iterabile e un iteratore non deve sempre memorizzare i suoi valori in memoria, a seconda dell'implementazione del metodo iter , può anche generare valori nella sequenza su richiesta.
picmate 涅

Sarebbe bello aggiungere a questa grande risposta il motivo per cui è lo stesso, tranne che hai usato ()invece di[] , in particolare ciò che ()è (potrebbe esserci confusione con una tupla).
WoJ

Potrei sbagliarmi, ma un generatore non è un iteratore, un "generatore chiamato" è un iteratore.
aderchox

@MatthiasFripp "Continua fino alla fine della funzione" o incontra returnun'istruzione. ( returnè consentito in una funzione contenente yield, purché non specifichi un valore di ritorno.)
alaniwi

2007

Scorciatoia per la comprensione yield

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

  1. Inserire una riga result = []all'inizio della funzione.
  2. Sostituisci ciascuno yield exprcon result.append(expr).
  3. Inserisci una riga return resultnella parte inferiore della funzione.
  4. Yay - non di più yield dichiarazioni! Leggi e scopri 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 yieldè significativamente diverso da ciò che accade nell'approccio basato sull'elenco. In molti casi, l'approccio alla resa 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 Iterabili, Iteratori e Generatori

Innanzitutto, il protocollo iteratore - quando scrivi

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

Python esegue i seguenti due passaggi:

  1. Ottiene un iteratore per mylist:

    Chiama iter(mylist)-> restituisce un oggetto con un next()metodo (o__next__() in Python 3).

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

  2. Utilizza l'iteratore per scorrere in sequenza gli elementi:

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

La verità è che Python esegue i due passaggi precedenti ogni volta che vuole passare in rassegna il contenuto di un oggetto - quindi potrebbe essere un ciclo for, ma potrebbe anche essere simile al codice otherlist.extend(mylist)(dove si otherlisttrova un elenco Python).

Ecco mylistun iterabile perché implementa il protocollo iteratore. In una classe definita dall'utente, è possibile implementare il __iter__()metodo per rendere iterabili le istanze della classe. Questo metodo dovrebbe restituire un iteratore . Un iteratore è un oggetto con un next()metodo. È possibile implementare sia __iter__()e next()sulla stessa classe, sia __iter__()restituireself . Funzionerà per casi semplici, ma non quando si desidera che due iteratori eseguano il ciclo sullo stesso oggetto contemporaneamente.

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

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

Nota che un forciclo non sa con quale tipo di oggetto ha a che fare: segue semplicemente il protocollo iteratore ed è felice di ottenere un oggetto dopo l'altro mentre lo chiama next(). Gli elenchi incorporati restituiscono i loro elementi uno per uno, i dizionari restituiscono i tasti uno per uno, i file restituiscono le righe uno per uno, ecc. E i generatori restituiscono ... beh, ecco dove yieldarriva:

def f123():
    yield 1
    yield 2
    yield 3

for item in f123():
    print item

Invece di yieldistruzioni, se tu avessi tre returnistruzioni f123()solo la prima verrebbe eseguita e la funzione uscirà. Ma f123()non è una funzione ordinaria. Quando f123()viene chiamato, non restituisce nessuno dei valori nelle dichiarazioni di rendimento! Restituisce un oggetto generatore. Inoltre, la funzione non si chiude davvero - va in uno stato sospeso. Quando il forloop tenta di eseguire il loop sull'oggetto generatore, la funzione riprende dal suo stato sospeso nella riga successiva dopo la yieldprecedente restituzione, esegue la riga di codice successiva, in questo caso, yieldun'istruzione e restituisce quella come successiva articolo. Questo succede fino a quando la funzione non esce, a quel punto il generatore si alzaStopIteration e il circuito termina.

Quindi l'oggetto generatore è un po 'come un adattatore - ad un'estremità mostra il protocollo iteratore, esponendo __iter__()e next()metodi per rendere forfelice il ciclo. All'altra estremità, tuttavia, esegue la funzione quanto basta per ottenere il valore successivo da esso e lo riporta in modalità sospesa.

Perché usare i generatori?

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


20
"Quando vedi una funzione con dichiarazioni di rendimento, applica questo semplice trucco per capire cosa accadrà" Questo non ignora completamente il fatto che puoi sendin un generatore, che è una parte enorme del punto dei generatori?
DanielSank

10
"potrebbe essere un ciclo for, ma potrebbe anche essere un codice come otherlist.extend(mylist)" -> Questo non è corretto. extend()modifica l'elenco sul posto e non restituisce un iterabile. Cercare di eseguire il loop over otherlist.extend(mylist)fallirà con un TypeErrorperché extend()restituisce implicitamente Nonee non è possibile eseguire il loop over None.
Pedro,

4
@pedro Hai frainteso quella frase. Significa che python esegue i due passaggi sopra citati mylist(non attivati otherlist) durante l'esecuzione otherlist.extend(mylist).
oggi

555

Pensare in questo modo:

Un iteratore è solo un termine dal suono elaborato per un oggetto che ha un next()metodo. Quindi una funzione snervata finisce per essere qualcosa del genere:

Versione originale:

def some_function():
    for i in xrange(4):
        yield i

for i in some_function():
    print i

Questo è fondamentalmente ciò che fa l'interprete Python con il codice sopra:

class it:
    def __init__(self):
        # Start at -1 so that we get 0 when we add 1 below.
        self.count = -1

    # The __iter__ method will be called once by the 'for' loop.
    # The rest of the magic happens on the object returned by this method.
    # In this case it is the object itself.
    def __iter__(self):
        return self

    # The next method will be called repeatedly by the 'for' loop
    # until it raises StopIteration.
    def next(self):
        self.count += 1
        if self.count < 4:
            return self.count
        else:
            # A StopIteration exception is raised
            # to signal that the iterator is done.
            # This is caught implicitly by the 'for' loop.
            raise StopIteration

def some_func():
    return it()

for i in some_func():
    print i

Per ulteriori informazioni su ciò che sta accadendo dietro le quinte, il forloop può essere riscritto in questo modo:

iterator = some_func()
try:
    while 1:
        print iterator.next()
except StopIteration:
    pass

Ha più senso o ti confonde di più? :)

Devo notare che questa è una semplificazione eccessiva a fini illustrativi. :)


1
__getitem__potrebbe essere definito invece di __iter__. Ad esempio class it: pass; it.__getitem__ = lambda self, i: i*10 if i < 10 else [][0]; for i in it(): print(i):, Stampa: 0, 10, 20, ..., 90
jfs

17
Ho provato questo esempio in Python 3.6 e se creo iterator = some_function(), la variabile iteratornon ha più una funzione chiamata next(), ma solo una __next__()funzione. Ho pensato di menzionarlo.
Peter,

Dove l' forimplementazione del ciclo che hai scritto chiama il __iter__metodo iterator, l'istanza istanziata di it?
Disintegrazione sistematica

455

La yieldparola chiave è ridotta a due semplici fatti:

  1. Se il compilatore rileva la yieldparola chiave in un punto qualsiasi all'interno di una funzione, tale funzione non ritorna più tramite l' returnistruzione. Al contrario , restituisce immediatamente un oggetto "lista in sospeso" pigro chiamato generatore
  2. Un generatore è iterabile. Che cos'è un iterabile ? È qualcosa di simile a una listo seto rangeo dict-view, con un protocollo integrato per visitare ogni elemento in un certo ordine .

In poche parole: un generatore è un elenco pigro, in attesa incrementale , e le yieldistruzioni consentono di utilizzare la notazione di funzione per programmare i valori dell'elenco che il generatore dovrebbe 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 makeRangeche è proprio come quella di Python range. Chiamata makeRange(n)TORNA A 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 potresti fare qualsiasi altro):

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

Esempio di confronto con "solo la restituzione di un elenco"

L'esempio sopra può essere pensato semplicemente come la creazione di un elenco che 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ò; vedi l'ultima sezione.


Come potresti usare i generatori

Un iterabile è l'ultima parte della comprensione di un elenco e tutti i generatori sono iterabili, quindi vengono spesso usati in questo modo:

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

Per avere un'idea migliore dei generatori, puoi giocare con il itertoolsmodulo (assicurati di usare chain.from_iterablepiuttosto che chainquando garantito). Ad esempio, potresti persino utilizzare i generatori per implementare elenchi pigri infinitamente lunghi come itertools.count(). Potresti implementare il tuo def enumerate(iterable): zip(count(), iterable), o in alternativa farlo conyield parola chiave in un ciclo while.

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


Dietro le quinte

Ecco come funziona il "protocollo di iterazione Python". Cioè, cosa sta succedendo quando lo fai 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 integrata next()chiama semplicemente la .next()funzione oggetti , che fa parte del "protocollo di iterazione" e si trova su tutti gli iteratori. Puoi usare manualmente la next()funzione (e altre parti del protocollo di iterazione) per implementare cose fantasiose, 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 iterabile è qualsiasi oggetto che "capisce il concetto di for-loop" come un elenco [1,2,3], e un iteratore è un'istanza specifica del for-loop richiesto [1,2,3].__iter__(). Un generatore è esattamente uguale a qualsiasi iteratore, ad eccezione del modo in cui è stato scritto (con 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 faresti raramente), ti dà solo una copia di se stesso.

Pertanto, nell'improbabile caso in cui non si 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è è monouso. Se si desidera riutilizzarlo, è necessario chiamare myRange(...)nuovamente. Se è necessario utilizzare due volte il risultato, convertirlo 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 eseguendo metaprogrammazioni terribilmente hackerate) possono usare itertools.teese assolutamente necessario, poiché la proposta di standard di iteratore Python PEP copiabile è stata rinviata.


378

Cosa fa la yieldparola chiave in Python?

Profilo di risposta / Riepilogo

  • Una funzione con yield, quando chiamata, restituisce un generatore .
  • I generatori sono iteratori perché implementano il protocollo iteratore , quindi puoi iterare su di essi.
  • Un generatore può anche essere inviato informazioni , rendendolo concettualmente un coroutine .
  • In Python 3, puoi delegare da un generatore all'altro in entrambe le direzioni conyield from .
  • (L'Appendice critica un paio di risposte, inclusa la prima, e discute l'uso di returnin un generatore.)

generatori:

yieldè legale solo all'interno di una definizione di funzione e l'inclusione yieldin una definizione di funzione fa sì che restituisca un generatore.

L'idea per i generatori viene da altre lingue (vedi nota 1) con implementazioni diverse. In Python's Generators, l'esecuzione del codice è bloccata nel punto del rendimento. Quando viene chiamato il generatore (i metodi sono discussi di seguito) l'esecuzione riprende e quindi si blocca alla resa successiva.

yieldfornisce un modo semplice per implementare il protocollo iteratore , definito dai due metodi seguenti: __iter__e next(Python 2) o __next__(Python 3). Entrambi questi metodi trasformano un oggetto in un iteratore che puoi controllare con la Iteratorclasse base astratta dal collectionsmodulo.

>>> def func():
...     yield 'I am'
...     yield 'a generator!'
... 
>>> type(func)                 # A function with yield is still a function
<type 'function'>
>>> gen = func()
>>> type(gen)                  # but it returns a generator
<type 'generator'>
>>> hasattr(gen, '__iter__')   # that's an iterable
True
>>> hasattr(gen, 'next')       # and with .next (.__next__ in Python 3)
True                           # implements the iterator protocol.

Il tipo di generatore è un sottotipo di iteratore:

>>> import collections, types
>>> issubclass(types.GeneratorType, collections.Iterator)
True

E se necessario, possiamo digitare in questo modo:

>>> isinstance(gen, types.GeneratorType)
True
>>> isinstance(gen, collections.Iterator)
True

Una caratteristica di un Iterator è che una volta esaurito , non è possibile riutilizzarlo o ripristinarlo:

>>> list(gen)
['I am', 'a generator!']
>>> list(gen)
[]

Dovrai crearne un altro se vuoi riutilizzare la sua funzionalità (vedi nota 2):

>>> list(func())
['I am', 'a generator!']

Si possono produrre dati a livello di codice, ad esempio:

def func(an_iterable):
    for item in an_iterable:
        yield item

Il generatore semplice sopra è anche equivalente al seguente - a partire da Python 3.3 (e non disponibile in Python 2), puoi usare yield from:

def func(an_iterable):
    yield from an_iterable

Tuttavia, yield fromconsente anche la delega ai sottogeneratori, che verrà spiegata nella sezione seguente sulla delega cooperativa con sottocoroutine.

coroutine:

yield forma un'espressione che consente l'invio di dati nel generatore (vedi nota 3)

Ecco un esempio, prendi nota della receivedvariabile, che punterà ai dati che vengono inviati al generatore:

def bank_account(deposited, interest_rate):
    while True:
        calculated_interest = interest_rate * deposited 
        received = yield calculated_interest
        if received:
            deposited += received


>>> my_account = bank_account(1000, .05)

In primo luogo, dobbiamo coda del generatore con la funzione built-in, next. Chiamerà l'appropriato nexto il __next__metodo, a seconda della versione di Python che si sta utilizzando:

>>> first_year_interest = next(my_account)
>>> first_year_interest
50.0

E ora possiamo inviare dati nel generatore. (L' invio Noneè lo stesso della chiamatanext .):

>>> next_year_interest = my_account.send(first_year_interest + 1000)
>>> next_year_interest
102.5

Delegazione cooperativa alla sottocoroutine con yield from

Ora, ricorda che yield fromè disponibile in Python 3. Questo ci consente di delegare le coroutine a una subcoroutine:

def money_manager(expected_rate):
    under_management = yield     # must receive deposited value
    while True:
        try:
            additional_investment = yield expected_rate * under_management 
            if additional_investment:
                under_management += additional_investment
        except GeneratorExit:
            '''TODO: write function to send unclaimed funds to state'''
        finally:
            '''TODO: write function to mail tax info to client'''


def investment_account(deposited, manager):
    '''very simple model of an investment account that delegates to a manager'''
    next(manager) # must queue up manager
    manager.send(deposited)
    while True:
        try:
            yield from manager
        except GeneratorExit:
            return manager.close()

E ora possiamo delegare la funzionalità a un sotto-generatore e può essere utilizzata da un generatore come sopra:

>>> my_manager = money_manager(.06)
>>> my_account = investment_account(1000, my_manager)
>>> first_year_return = next(my_account)
>>> first_year_return
60.0
>>> next_year_return = my_account.send(first_year_return + 1000)
>>> next_year_return
123.6

Puoi leggere di più sulla semantica precisa di yield fromin PEP 380.

Altri metodi: chiudi e lancia

Il closemetodo aumenta GeneratorExitnel punto in cui l'esecuzione della funzione è stata congelata. Questo verrà anche chiamato da __del__così puoi inserire qualsiasi codice di pulizia in cui gestisci GeneratorExit:

>>> my_account.close()

Puoi anche generare un'eccezione che può essere gestita nel generatore o propagata all'utente:

>>> import sys
>>> try:
...     raise ValueError
... except:
...     my_manager.throw(*sys.exc_info())
... 
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
  File "<stdin>", line 2, in <module>
ValueError

Conclusione

Credo di aver coperto tutti gli aspetti della seguente domanda:

Cosa fa la yieldparola chiave in Python?

Si scopre che yieldfa molto. Sono sicuro di poter aggiungere esempi ancora più approfonditi a questo. Se vuoi di più o hai delle critiche costruttive, fammi sapere commentando qui sotto.


Appendice:

Critica della risposta migliore / accettata **

  • È confuso su ciò che rende un iterabile , usando solo un elenco come esempio. Vedi i miei riferimenti sopra, ma in sintesi: un iterabile ha un __iter__metodo che restituisce un iteratore . Un iteratore fornisce un metodo .next(Python 2 o .__next__(Python 3), che viene implicitamente chiamato dai forloop fino a quando non generaStopIteration e, una volta fatto, continuerà a farlo.
  • Quindi utilizza un'espressione del generatore per descrivere cos'è un generatore. Poiché un generatore è semplicemente un modo conveniente per creare un iteratore , confonde solo la questione e non siamo ancora arrivati ​​alla yieldparte.
  • Nel controllare l'esaurimento di un generatore chiama il .nextmetodo, quando invece dovrebbe usare la funzione incorporata,next . Sarebbe un livello appropriato di riferimento indiretto, perché il suo codice non funziona in Python 3.
  • Itertools? Questo non era rilevante per quello che yieldfa affatto.
  • Nessuna discussione sui metodi yieldforniti con le nuove funzionalità yield fromin Python 3. La risposta migliore / accettata è una risposta molto incompleta.

Critica della risposta che suggerisce yieldun'espressione o comprensione del generatore.

La grammatica attualmente consente qualsiasi espressione nella comprensione di un elenco.

expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) |
                     ('=' (yield_expr|testlist_star_expr))*)
...
yield_expr: 'yield' [yield_arg]
yield_arg: 'from' test | testlist

Poiché la resa è un'espressione, è stato pubblicizzato da alcuni come interessante usarlo nelle comprensioni o nell'espressione del generatore - nonostante non abbia citato casi d'uso particolarmente validi.

Gli sviluppatori core di CPython stanno discutendo di deprecare la sua tolleranza . Ecco un post pertinente dalla mailing list:

Il 30 gennaio 2017 alle 19:05, Brett Cannon ha scritto:

Il domenica 29 gennaio 2017 alle 16:39 Craig Rodrigues ha scritto:

Sto bene con entrambi gli approcci. Lasciare le cose come sono in Python 3 non va bene, IMHO.

Il mio voto è che si tratta di un SyntaxError poiché non si ottiene ciò che ci si aspetta dalla sintassi.

Concordo sul fatto che sia un posto ragionevole per noi finire, poiché qualsiasi codice basato sul comportamento attuale è davvero troppo intelligente per essere mantenibile.

In termini di arrivarci, probabilmente vorremmo:

  • Sintassi Avvertenza o disapprovazione Avvertenza in 3.7
  • Avviso Py3k in 2.7.x
  • Sintassi Errore nel 3.8

Saluti, Nick.

- Nick Coghlan | ncoghlan su gmail.com | Brisbane, Australia

Inoltre, c'è un problema eccezionale (10544) che sembra puntare nella direzione in cui questa non è mai una buona idea (PyPy, un'implementazione di Python scritta in Python, sta già generando avvisi di sintassi.)

In conclusione, fino a quando gli sviluppatori di CPython non ci diranno diversamente: non inserire yieldun'espressione o una comprensione del generatore.

La returndichiarazione in un generatore

In Python 2 :

In una funzione generatore, l' returnistruzione non può includere un expression_list. In quel contesto, un nudo returnindica che il generatore è terminato e farà StopIterationalzare.

Un expression_listè praticamente un numero qualsiasi di espressioni separate da virgole - essenzialmente, in Python 2, puoi fermare il generatore con return, ma non puoi restituire un valore.

In Python 3 :

In una funzione generatore, l' returnistruzione indica che il generatore è terminato e causerà il StopIterationsollevamento. Il valore restituito (se presente) viene utilizzato come argomento per costruire StopIteratione diventa l' StopIteration.valueattributo.

Le note

  1. Le lingue CLU, Sather e Icon sono state citate nella proposta per introdurre il concetto di generatori in Python. L'idea generale è che una funzione può mantenere lo stato interno e produrre punti dati intermedi su richiesta dell'utente. Ciò ha promesso di essere superiore in termini di prestazioni ad altri approcci, incluso il threading Python , che non è nemmeno disponibile su alcuni sistemi.

  2. Ciò significa, ad esempio, che gli xrangeoggetti ( rangein Python 3) non sono Iterator, anche se sono iterabili, perché possono essere riutilizzati. Come gli elenchi, i loro __iter__metodi restituiscono oggetti iteratore.

  3. yieldè stato originariamente introdotto come un'istruzione, il che significa che poteva apparire solo all'inizio di una riga in un blocco di codice. Ora yieldcrea un'espressione di resa. https://docs.python.org/2/reference/simple_stmts.html#grammar-token-yield_stmt Questa modifica è stata proposta per consentire a un utente di inviare dati nel generatore così come si potrebbe ricevere. Per inviare dati, si deve essere in grado di assegnarli a qualcosa e, per questo, un'istruzione non funzionerà.


328

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

Nel caso del codice, la funzione get_child_candidatessi comporta come un iteratore in modo che quando si estende l'elenco, si aggiunge un elemento alla volta al nuovo elenco.

list.extendchiama un iteratore fino a quando non è esaurito. Nel caso dell'esempio di codice che hai pubblicato, sarebbe molto più chiaro restituire una tupla e aggiungerla all'elenco.


107
Questo è vicino, ma non corretto. Ogni volta che si chiama una funzione con una dichiarazione di rendimento, restituisce un oggetto generatore nuovo di zecca. È solo quando chiami il metodo .next () di quel generatore che l'esecuzione riprende dopo l'ultimo rendimento.
Kurosch,

239

C'è una cosa in più da menzionare: una funzione che produce non deve effettivamente terminare. Ho scritto codice in questo modo:

def fib():
    last, cur = 0, 1
    while True: 
        yield cur
        last, cur = cur, last + cur

Quindi posso usarlo in altri codici come questo:

for f in fib():
    if some_condition: break
    coolfuncs(f);

Aiuta davvero a semplificare alcuni problemi e rende alcune cose più facili da lavorare.


233

Per coloro che preferiscono un esempio minimo di lavoro, medita 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

209

TL; DR

Invece di questo:

def square_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 square_yield(n):
    for x in range(n):
        y = x * x
        yield y                           # with this one.

Ogni volta che ti ritrovi a costruire un elenco da zero, yieldogni pezzo invece.

Questo è stato il mio primo momento "aha" con la resa.


yieldè un modo zuccherino per dire

costruire una serie di cose

Stesso comportamento:

>>> for square in square_list(4):
...     print(square)
...
0
1
4
9
>>> for square in square_yield(4):
...     print(square)
...
0
1
4
9

Comportamento diverso:

La resa è single-pass : puoi iterare una sola volta. Quando una funzione ha un rendimento, la chiamiamo funzione generatore . E un iteratore è ciò che restituisce. Quei termini sono rivelatori. Perdiamo la comodità di un container, ma otteniamo la potenza di una serie calcolata secondo necessità e arbitrariamente lunga.

La resa è pigra , rimanda il calcolo. Una funzione con un rendimento in essa non viene eseguita affatto quando la chiami. Restituisce un oggetto iteratore che ricorda da dove era stato interrotto. Ogni volta che inviti next()all'iteratore (questo accade in un ciclo for), l'esecuzione si sposta in avanti verso il rendimento successivo. returngenera StopIteration e termina la serie (questa è la fine naturale di un for-loop).

La resa è versatile . I dati non devono essere archiviati tutti insieme, possono essere resi disponibili uno alla volta. Può essere infinito.

>>> 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ù passaggi e la serie non è troppo lunga, chiamala semplicemente list():

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

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

resa - produrre o fornire (come in agricoltura)

... fornire i dati successivi nella serie.

cedere - cedere o arrendersi (come nel potere politico)

... rinuncia all'esecuzione della CPU fino a quando l'iteratore non avanza.


194

La resa ti dà un generatore.

def get_odd_numbers(i):
    return range(1, i, 2)
def yield_odd_numbers(i):
    for x in range(1, i, 2):
       yield x
foo = get_odd_numbers(10)
bar = yield_odd_numbers(10)
foo
[1, 3, 5, 7, 9]
bar
<generator object yield_odd_numbers at 0x1029c6f50>
bar.next()
1
bar.next()
3
bar.next()
5

Come puoi vedere, nel primo caso foocontiene l'intero elenco in memoria contemporaneamente. Non è un grosso problema per un elenco con 5 elementi, ma cosa succede se si desidera un elenco di 5 milioni? Non solo questo è un enorme mangiatore di memoria, ma costa anche molto tempo per essere costruito nel momento in cui viene chiamata la funzione.

Nel secondo caso, barti dà solo un generatore. Un generatore è un iterabile, il che significa che puoi usarlo in un forciclo, ecc., Ma ogni valore è accessibile solo una volta. Inoltre, tutti i valori non vengono memorizzati contemporaneamente; l'oggetto generatore "ricorda" dove si trovava nel loop l'ultima volta che lo hai chiamato - in questo modo, se stai usando un iterabile per (diciamo) contare fino a 50 miliardi, non devi contare fino a 50 miliardi tutti subito e memorizza i 50 miliardi di numeri da contare.

Ancora una volta, questo è un esempio piuttosto ingegnoso, probabilmente useresti itertools se volessi davvero contare fino a 50 miliardi. :)

Questo è il caso d'uso più semplice dei generatori. Come hai detto, può essere usato per scrivere permutazioni efficienti, usando il rendimento per spingere le cose verso l'alto attraverso lo stack di chiamate invece di usare una sorta di variabile di stack. I generatori possono anche essere utilizzati per attraversamenti di alberi specializzati e ogni altra cosa.


Solo una nota: in Python 3 rangerestituisce anche un generatore anziché un elenco, quindi vedresti anche un'idea simile, tranne per il fatto che __repr__/ __str__vengono sovrascritti per mostrare un risultato migliore, in questo caso range(1, 10, 2).
It'sNotALie.

189

Sta restituendo un generatore. Non conosco particolarmente Python, ma credo che sia lo stesso tipo di blocchi di iteratori di C # se hai familiarità con quelli.

L'idea chiave è che il compilatore / interprete / qualunque cosa faccia qualche trucco in modo che, per quanto riguarda il chiamante, possano continuare a chiamare next () e continuerà a restituire valori - come se il metodo del generatore fosse in pausa . Ora ovviamente non puoi davvero "mettere in pausa" un metodo, quindi il compilatore crea una macchina a stati per ricordarti dove ti trovi attualmente e come sono le variabili locali ecc. Questo è molto più semplice che scrivere un iteratore da soli.


167

C'è un tipo di risposta che non credo sia stata ancora data, tra le molte 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 vengono spesso utilizzate, poiché sono estremamente difficili da ragionare e anche molto difficili da implementare. Ma l'idea di cosa sia una continuazione è semplice: è lo stato di un calcolo che non è ancora finito. In questo stato, vengono salvati i valori correnti delle variabili, le operazioni che devono ancora essere eseguite e così via. Quindi ad un certo punto nel programma successivo è possibile invocare la continuazione, in modo tale che le variabili del programma vengano reimpostate su quello stato e vengano eseguite le operazioni salvate.

Le continuazioni, in questa forma più generale, possono essere implementate in due modi. A call/ccproposito, lo stack del programma viene letteralmente salvato e quindi quando viene invocata la continuazione, lo stack viene ripristinato.

Nello stile di passaggio di continuazione (CPS), le continuazioni sono solo normali funzioni (solo nei linguaggi in cui le funzioni sono di prima classe) che il programmatore gestisce esplicitamente e passa alle subroutine. In questo stile, lo stato del programma è rappresentato dalle chiusure (e dalle variabili che si trovano in esse codificate) piuttosto che dalle variabili che risiedono da qualche parte nello stack. Le funzioni che gestiscono il flusso di controllo accettano la continuazione come argomenti (in alcune varianti di CPS, le funzioni possono accettare continuazioni multiple) e manipolano il flusso di controllo invocandoli semplicemente chiamandoli e ritornando in seguito. Un esempio molto semplice di stile di passaggio di seguito è 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 scrittura effettiva del file in una continuazione (che può potenzialmente essere un'operazione molto complessa con molti dettagli da scrivere), quindi passa quella continuazione (ovvero, come prima- chiusura di classe) a un altro operatore che esegue un po 'più di elaborazione e quindi lo chiama se necessario. (Uso molto questo modello di progettazione nella programmazione effettiva della GUI, sia perché mi fa risparmiare 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 da leggere.


Ora parliamo di generatori in Python. I generatori sono un sottotipo specifico 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 . Tuttavia, 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 scorre su di esso, restituisce 4 (e lo fa per sempre). Ma non è probabilmente il tipo prototipico di iterabile che viene in mente quando si pensa agli iteratori (cioè, for x in collection: do_something(x)). Questo esempio illustra la potenza dei generatori: se qualcosa è un iteratore, un generatore può salvare lo stato della sua iterazione.

Per ripetere: le continuazioni possono salvare lo stato dello stack di un programma e i generatori possono salvare lo stato dell'iterazione. Ciò significa che le continuazioni sono molto più potenti dei generatori, ma anche che i generatori sono molto, molto più facili. Sono più facili da implementare per il progettista del linguaggio e sono più facili da usare per il programmatore (se hai tempo da perdere, prova a leggere e comprendere questa pagina sulle continuazioni e chiama / cc ).

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

Ogni volta che yieldviene chiamato, indica alla funzione di restituire una continuazione. Quando la funzione viene richiamata di nuovo, inizia da dove era stata interrotta. Quindi, nello pseudo-pseudocodice (cioè non nello pseudocodice, ma non nel codice) il nextmetodo del generatore è sostanzialmente 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 vera funzione del generatore, fondamentalmente qualcosa di simile a:

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'implementazione effettiva dei generatori in Python è più complessa. Ma come esercizio per capire cosa sta succedendo, prova a usare lo stile di passaggio di continuazione per implementare oggetti del generatore senza usare la yieldparola chiave.


152

Ecco un esempio in un linguaggio semplice. Fornirò una corrispondenza tra concetti umani di alto livello e concetti Python di basso livello.

Voglio operare su una sequenza di numeri, ma non voglio disturbarmi con la creazione di quella sequenza, voglio solo concentrarmi sull'operazione che voglio fare. Quindi, faccio quanto segue:

  • Ti chiamo e ti dico che voglio una sequenza di numeri che viene prodotta in un modo specifico e ti faccio sapere qual è l'algoritmo.
    Questo passaggio corrisponde defall'inserimento della funzione generatore, ovvero alla funzione contenente a yield.
  • Qualche tempo dopo, ti dico "OK, preparati a dirmi la sequenza di numeri".
    Questo passaggio corrisponde alla chiamata della funzione generatore che restituisce un oggetto generatore. Nota che non mi dici ancora nessun numero; prendi solo carta e matita.
  • Ti chiedo "dimmi il prossimo numero" e tu mi dici il primo numero; dopo quello, aspetti che io ti chieda il prossimo numero. Il tuo compito è ricordare dove eri, quali numeri hai già detto e qual è il prossimo numero. Non mi interessano i dettagli.
    Questo passaggio corrisponde alla chiamata .next()sull'oggetto generatore.
  • ... ripeti il ​​passaggio precedente, fino a ...
  • alla fine, potresti finire. Non mi dici un numero; tu gridi semplicemente "tieni i tuoi cavalli! Ho finito! Niente più numeri!"
    Questo passaggio corrisponde all'oggetto generatore che termina il suo lavoro e genera StopIterationun'eccezione La funzione generatore non deve sollevare l'eccezione. Viene generato automaticamente quando la funzione termina o genera a return.

Questo è ciò che fa un generatore (una funzione che contiene a yield); inizia l'esecuzione, si interrompe ogni volta che fa un yielde quando viene richiesto un .next()valore continua dal punto in cui è stato l'ultimo. Si adatta perfettamente alla progettazione con il protocollo iteratore di Python, che descrive come richiedere sequenzialmente i valori.

L'utente più famoso del protocollo iteratore è il forcomando in Python. Quindi, ogni volta che fai un:

for item in sequence:

non importa se sequenceè un elenco, una stringa, un dizionario o un oggetto generatore come descritto sopra; il risultato è lo stesso: leggi gli articoli in sequenza uno per uno.

Notare che l' definserimento di una funzione che contiene una yieldparola chiave non è l'unico modo per creare un generatore; è solo il modo più semplice per crearne uno.

Per informazioni più accurate, leggi i tipi di iteratore , la dichiarazione di rendimento e i generatori nella documentazione di Python.


130

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

Per aiutare a capire cosa yieldfa un nel seguente codice, puoi usare il dito per tracciare il ciclo attraverso qualsiasi codice che ha un yield. Ogni volta che si preme il dito su yield, è necessario attendere l'inserimento di a nexto senda. Quando nextviene chiamato un, si traccia attraverso il codice fino a quando non si preme il yield... il codice a destra del yieldviene valutato e restituito al chiamante ... quindi si attende. Quando nextviene chiamato di nuovo, si esegue un altro ciclo attraverso il codice. Tuttavia, noterai che in un coroutine, yieldpuò anche essere usato con un send... che invierà un valore dal chiamante nella funzione cedente. Se sendviene dato un, allorayieldriceve il valore inviato e lo sputa dal lato sinistro ... quindi la traccia attraverso il codice procede fino a quando non si preme di yieldnuovo (restituendo il valore alla fine, come se nextfosse 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()

Carina! Un trampolino (nel senso di Lisp). Non spesso si vedono quelli!
00prometheus

129

C'è un altro yielduso e significato (da Python 3.3):

yield from <expr>

Da PEP 380 - Sintassi per delegare a un sottogeneratore :

Viene proposta una sintassi per un generatore per delegare parte delle sue operazioni a un altro generatore. Ciò consente a una sezione di codice contenente 'yield' di essere fattorizzata e posizionata in un altro generatore. Inoltre, il sottogeneratore può tornare con un valore e il valore viene reso disponibile al generatore delegante.

La nuova sintassi offre anche alcune opportunità di ottimizzazione quando un generatore restituisce valori prodotti da un altro.

Inoltre questo introdurrà (dal Python 3.5):

async def new_coroutine(data):
   ...
   await blocking_action()

per evitare che le coroutine vengano confuse con un normale generatore (oggi yieldviene utilizzato in entrambi).


117

Tutte ottime risposte, tuttavia un po 'difficili per i neofiti.

Presumo che tu abbia imparato la returndichiarazione.

Come analogia, returne yieldsono gemelli. returnsignifica "ritorno e arresto" mentre "rendimento" significa "ritorno, ma continua"

  1. Prova a ottenere un num_list con return.
def num_list(n):
    for i in range(n):
        return i

Eseguirlo:

In [5]: num_list(3)
Out[5]: 0

Vedi, ottieni un solo numero anziché un elenco di essi. returnnon ti permette mai di prevalere felicemente, implementa solo una volta ed esci.

  1. Arriva yield

Sostituisci returncon yield:

In [10]: def num_list(n):
    ...:     for i in range(n):
    ...:         yield i
    ...:

In [11]: num_list(3)
Out[11]: <generator object num_list at 0x10327c990>

In [12]: list(num_list(3))
Out[12]: [0, 1, 2]

Ora vinci per ottenere tutti i numeri.

Rispetto a returnquale viene eseguito una volta e si interrompe, yieldesegue i tempi pianificati. Puoi interpretare returncome return one of theme yieldcome return all of them. Questo si chiama iterable.

  1. Un altro passo con cui possiamo riscrivere la yielddichiarazionereturn
In [15]: def num_list(n):
    ...:     result = []
    ...:     for i in range(n):
    ...:         result.append(i)
    ...:     return result

In [16]: num_list(3)
Out[16]: [0, 1, 2]

È il nocciolo della questione yield.

La differenza tra l' returnoutput di un elenco e l' yieldoutput dell'oggetto è:

Otterrai sempre [0, 1, 2] da un oggetto elenco ma potresti recuperarli solo yielduna volta dall'output dell'oggetto . Quindi, ha un nuovo generatoroggetto nome come visualizzato in Out[11]: <generator object num_list at 0x10327c990>.

In conclusione, come metafora per grok:

  • returne yieldsono gemelli
  • liste generatorsono gemelli

Questo è comprensibile, ma una delle principali differenze è che puoi avere rese multiple in una funzione / metodo. L'analogia si interrompe totalmente a quel punto. Yield ricorda il suo posto in una funzione, quindi la prossima volta che chiami next (), la tua funzione continua con quella successiva yield. Questo è importante, penso, e dovrebbe essere espresso.
Mike S,

104

Ecco alcuni esempi di Python su come implementare effettivamente i generatori come se Python non fornisse loro zucchero sintattico:

Come generatore Python:

from itertools import islice

def fib_gen():
    a, b = 1, 1
    while True:
        yield a
        a, b = b, a + b

assert [1, 1, 2, 3, 5] == list(islice(fib_gen(), 5))

Utilizzo di chiusure lessicali anziché generatori

def ftake(fnext, last):
    return [fnext() for _ in xrange(last)]

def fib_gen2():
    #funky scope due to python2.x workaround
    #for python 3.x use nonlocal
    def _():
        _.a, _.b = _.b, _.a + _.b
        return _.a
    _.a, _.b = 0, 1
    return _

assert [1,1,2,3,5] == ftake(fib_gen2(), 5)

Utilizzo di chiusure di oggetti anziché generatori (perché ClosuresAndObjectsAreEquivalent )

class fib_gen3:
    def __init__(self):
        self.a, self.b = 1, 1

    def __call__(self):
        r = self.a
        self.a, self.b = self.b, self.a + self.b
        return r

assert [1,1,2,3,5] == ftake(fib_gen3(), 5)

97

Stavo per pubblicare "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 utilizzato nelle coroutine come doppio del loro utilizzo nelle funzioni del generatore. Sebbene non sia lo stesso uso del frammento di codice, (yield)può essere utilizzato 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 si incontra l' istruzione successiva .

Generatori e coroutine sono un ottimo modo per impostare applicazioni di tipo flusso di dati. Ho pensato che sarebbe utile conoscere l'altro uso yielddell'istruzione nelle funzioni.


97

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

Per implementare iteratori, generatori e pool di thread per l'esecuzione simultanea, ecc. Come thunk (chiamati anche funzioni anonime), si usano 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 ".

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

Ecco una dimostrazione che utilizza la struttura di R6RS, ma la semantica è assolutamente identica a quella di Python. È lo stesso modello di calcolo e per riscriverlo in Python è necessario solo un cambiamento nella sintassi.

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

84

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 ciclo successivo inizi dalla posizione "yield". Sembra che stia aspettando in quella posizione, e poco prima, restituendo un valore all'esterno e la prossima volta continuerà a funzionare.

Sembra essere un'abilità interessante e piacevole: D


Hai ragione. Ma qual è l'effetto sul flusso che è vedere il comportamento del "rendimento"? Posso cambiare l'algoritmo in nome della matematica. Aiuterà a ottenere una diversa valutazione del "rendimento"?
Engin OZTURK,

68

Ecco un'immagine mentale di ciò che yieldfa.

Mi piace pensare che un thread abbia uno stack (anche quando non è implementato in quel modo).

Quando viene chiamata una funzione normale, mette le sue variabili locali nello stack, esegue alcuni calcoli, quindi cancella lo stack e restituisce. I valori delle sue variabili locali non vengono mai più visualizzati.

Con una yieldfunzione, quando il suo codice inizia a funzionare (cioè dopo che la funzione è stata chiamata, restituendo un oggetto generatore, il cui next()metodo viene quindi invocato), allo stesso modo mette le sue variabili locali nello stack e calcola per un po '. Ma poi, quando colpisce l' yieldistruzione, prima di cancellare la sua parte dello stack e tornare, prende un'istantanea delle sue variabili locali e le memorizza nell'oggetto generatore. Inoltre, annota il punto in cui è attualmente nel suo codice (ovvero la specifica yieldistruzione).

Quindi è una specie di funzione congelata a cui il generatore è sospeso.

Quando next()viene chiamato successivamente, recupera gli effetti personali della funzione nello stack e lo rianima. La funzione continua a calcolare da dove era stata interrotta, ignaro del fatto che aveva appena trascorso un'eternità in celle frigorifere.

Confronta i seguenti esempi:

def normalFunction():
    return
    if False:
        pass

def yielderFunction():
    return
    if False:
        yield 12

Quando chiamiamo la seconda funzione, si comporta in modo molto diverso dalla prima. L' yieldaffermazione potrebbe essere irraggiungibile, ma se è presente ovunque, cambia la natura di ciò con cui abbiamo a che fare.

>>> yielderFunction()
<generator object yielderFunction at 0x07742D28>

La chiamata yielderFunction()non esegue il suo codice, ma rende un generatore fuori dal codice. (Forse è una buona idea nominare tali cose con il yielderprefisso per leggibilità.)

>>> gen = yielderFunction()
>>> dir(gen)
['__class__',
 ...
 '__iter__',    #Returns gen itself, to make it work uniformly with containers
 ...            #when given to a for loop. (Containers return an iterator instead.)
 'close',
 'gi_code',
 'gi_frame',
 'gi_running',
 'next',        #The method that runs the function's body.
 'send',
 'throw']

I campi gi_codee gi_framesono dove è memorizzato lo stato congelato. Esplorandoli con dir(..), possiamo confermare che il nostro modello mentale sopra è credibile.


59

Come suggerisce ogni risposta, yieldviene utilizzato per creare un generatore di sequenze. È usato per generare dinamicamente una sequenza. 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 per il controllo dell'esecuzione

Il controllo dell'esecuzione verrà trasferito da getNextLines () al forloop 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"

59

Un semplice esempio per capire di cosa si tratta: yield

def f123():
    for _ in range(4):
        yield 1
        yield 2


for i in f123():
    print (i)

L'output è:

1 2 1 2 1 2 1 2

5
sei sicuro di quell'output? non verrebbe stampato su una sola riga se si eseguisse quell'istruzione print usando print(i, end=' ')? Altrimenti, credo che il comportamento predefinito metterebbe ogni numero su una nuova riga
user9074332

@ user9074332, hai ragione, ma è scritto su una riga per facilitare la comprensione
Gavriel Cohen,

57

(La mia risposta di seguito parla solo dal punto di vista dell'utilizzo del generatore Python, non dell'implementazione sottostante del meccanismo del generatore , che comporta alcuni trucchi di manipolazione di stack e heap.)

Quando yieldviene utilizzata al posto di una funzione returnin una pitone, quella funzione viene trasformata in qualcosa di speciale chiamato generator function. Tale funzione restituirà un oggetto di generatortipo. La yieldparola chiave è un flag per avvisare il compilatore python di trattare tale funzione in modo speciale. Le normali funzioni termineranno dopo la restituzione di un valore. Ma con l'aiuto del compilatore, la funzione generatore può essere pensata come ripristinabile. Cioè, il contesto di esecuzione verrà ripristinato e l'esecuzione continuerà dall'ultima esecuzione. Fino a quando non si chiama esplicitamente return, che genererà StopIterationun'eccezione (che fa anche parte del protocollo iteratore) o raggiungerà la fine della funzione. Ho trovato molti riferimenti a riguardo, generatorma questo unodal functional programming perspectiveè il più digeribile.

(Ora voglio parlare della logica alla base generatore della iteratorbase della mia comprensione. Spero che questo possa aiutarti a cogliere la motivazione essenziale di iteratore e generatore. Tale concetto si manifesta in altre lingue come C #.)

A quanto ho capito, quando vogliamo elaborare un mucchio di dati, di solito memorizziamo prima i dati da qualche parte e quindi li elaboriamo uno per uno. Ma questo approccio ingenuo è problematico. Se il volume di dati è enorme, è costoso memorizzarli in anticipo. Quindi, invece di archiviare datadirettamente se stesso, perché non conservare una specie di metadataindirettamente, cioèthe 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 progettazione iteratore comunemente visto .
  2. L'approccio funzionale, avvolgiamo i metadati as a function. Questo è il cosiddetto generator function. Ma sotto il cofano, l' iteratore restituito è generator objectancora IS-Aperché implementa anche il protocollo iteratore.

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


54

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 ripetuto, esegue la tua funzione fino a raggiungere quella successiva, yieldquindi sospende l'esecuzione e valuta il valore passato a yield. Ripete questo processo su ogni iterazione fino a quando 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

La potenza deriva dall'utilizzo del generatore con un loop che calcola una sequenza, il generatore esegue il loop fermandosi ogni volta per "produrre" il risultato successivo del calcolo, in questo modo calcola un elenco al volo, il vantaggio è la memoria salvato per calcoli particolarmente grandi

Supponi di voler creare una tua rangefunzione che produca un intervallo iterabile di numeri, puoi farlo in questo modo,

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

e usalo in questo modo;

for i in myRangeNaive(10):
    print i

Ma questo è inefficiente perché

  • Si crea un array che si utilizza solo una volta (questo spreca memoria)
  • Questo codice scorre effettivamente su quell'array due volte! :(

Fortunatamente Guido e il suo team furono 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 ad ogni iterazione una funzione sul generatore chiamato next()esegue la funzione fino a quando non raggiunge un'istruzione 'yield' in cui si ferma e 'cede' il valore o raggiunge la fine della funzione. In questo caso alla prima chiamata, next()esegue fino all'istruzione yield e yield 'n', alla chiamata successiva eseguirà l'istruzione incrementale, tornerà al 'while', lo valuterà e, se vero, si fermerà e cedere di nuovo 'n', continuerà in questo modo fino a quando la condizione while non restituisce false e il generatore salta alla fine della funzione.


53

La resa è un oggetto

A returnin una funzione restituirà un singolo valore.

Se vuoi che una funzione restituisca un enorme set di valori , usa yield.

Ancora più importante, yieldè una barriera .

come barriera nel linguaggio CUDA, non trasferirà il controllo fino al completamento.

Cioè, eseguirà il codice nella tua funzione dall'inizio fino a quando non colpisce yield. Quindi, restituirà il primo valore del ciclo.

Quindi, ogni altra chiamata eseguirà il ciclo che hai scritto nella funzione ancora una volta, restituendo il valore successivo fino a quando non c'è alcun valore da restituire.


52

Molte persone usano returnpiuttosto che yield, ma in alcuni casi yieldpossono essere più efficienti e più facili da lavorare.

Ecco un esempio che yieldè decisamente migliore per:

ritorno (in funzione)

import random

def return_dates():
    dates = [] # With 'return' you need to create a list then return it
    for i in range(5):
        date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
        dates.append(date)
    return dates

resa (in funzione)

def yield_dates():
    for i in range(5):
        date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
        yield date # 'yield' makes a generator automatically which works
                   # in a similar way. This is much more efficient.

Funzioni di chiamata

dates_list = return_dates()
print(dates_list)
for i in dates_list:
    print(i)

dates_generator = yield_dates()
print(dates_generator)
for i in dates_generator:
    print(i)

Entrambe le funzioni fanno la stessa cosa, ma yieldusano tre righe anziché cinque e hanno una variabile in meno di cui preoccuparsi.

Questo è il risultato del codice:

Produzione

Come puoi vedere, entrambe le funzioni fanno la stessa cosa. L'unica differenza è che return_dates()fornisce un elenco e yield_dates()un generatore.

Un esempio di vita reale potrebbe essere qualcosa come leggere un file riga per riga o se vuoi semplicemente creare un generatore.


43

yieldè come un elemento di ritorno per una funzione. La differenza è che l' yieldelemento trasforma una funzione in un generatore. Un generatore si comporta proprio come una funzione finché qualcosa non viene "ceduto". Il generatore si arresta fino a quando non viene chiamato successivamente e continua esattamente dallo stesso punto in cui è stato avviato. Puoi ottenere una sequenza di tutti i valori "ceduti" in uno, chiamando list(generator()).


41

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


36

Ecco un semplice yieldapproccio basato, per calcolare la serie di fibonacci, spiegato:

def fib(limit=50):
    a, b = 0, 1
    for i in range(limit):
       yield b
       a, b = b, a+b

Quando lo inserisci nel tuo REPL e poi provi a chiamarlo, otterrai un risultato mistificante:

>>> fib()
<generator object fib at 0x7fa38394e3b8>

Questo perché la presenza di yieldPython ha segnalato che si desidera creare un generatore , ovvero un oggetto che genera valori su richiesta.

Quindi, come si generano questi valori? Questo può essere fatto direttamente utilizzando la funzione integrata nexto, indirettamente, alimentandolo a un costrutto che consuma valori.

Utilizzando il built-in next() funzione integrata, invochi direttamente .next/ __next__, forzando il generatore a produrre un valore:

>>> g = fib()
>>> next(g)
1
>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)
5

Indirettamente, se fornisci fib a un forciclo, un listinizializzatore, un tupleinizializzatore o qualsiasi altra cosa che si aspetta un oggetto che genera / produce valori, "consumerai" il generatore fino a quando non sarà più possibile produrre da esso (e restituisce) :

results = []
for i in fib(30):       # consumes fib
    results.append(i) 
# can also be accomplished with
results = list(fib(30)) # consumes fib

Allo stesso modo, con un tupleinizializzatore:

>>> tuple(fib(5))       # consumes fib
(1, 1, 2, 3, 5)

Un generatore differisce da una funzione nel senso che è pigro. A tale scopo, mantiene lo stato locale e consente di riprendere ogni volta che è necessario.

Quando invochi per la prima volta fib per la chiamandolo:

f = fib()

Python compila la funzione, incontra la yieldparola chiave e ti restituisce semplicemente un oggetto generatore. Sembra non molto utile.

Quando lo richiedi, genera il primo valore, direttamente o indirettamente, esegue tutte le istruzioni che trova, fino a quando non incontra un yield, quindi restituisce il valore che hai fornito yielde fa una pausa. Per un esempio che lo dimostra meglio, usiamo alcune printchiamate (sostituiscile con print "text"if su Python 2):

def yielder(value):
    """ This is an infinite generator. Only use next on it """ 
    while 1:
        print("I'm going to generate the value for you")
        print("Then I'll pause for a while")
        yield value
        print("Let's go through it again.")

Ora, inserisci REPL:

>>> gen = yielder("Hello, yield!")

ora hai un oggetto generatore in attesa di un comando per generare un valore. Usa nexte guarda cosa viene stampato:

>>> next(gen) # runs until it finds a yield
I'm going to generate the value for you
Then I'll pause for a while
'Hello, yield!'

I risultati non quotati sono quelli stampati. Il risultato citato è ciò che viene restituito yield. Richiama nextora:

>>> next(gen) # continues from yield and runs again
Let's go through it again.
I'm going to generate the value for you
Then I'll pause for a while
'Hello, yield!'

Il generatore ricorda che è stato messo in pausa yield valuee riprende da lì. Il messaggio successivo viene stampato e la ricerca yielddell'istruzione in pausa su di essa viene eseguita nuovamente (a causa del whileciclo).

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.