La parola "resa" ha due significati: produrre qualcosa (ad esempio, produrre mais) e fermarsi per far continuare qualcun altro (ad esempio, automobili che cedono ai pedoni). Entrambe le definizioni si applicano a Pythonyield parola chiave ; ciò che rende speciali le funzioni del generatore è che, diversamente dalle normali funzioni, i valori possono essere "restituiti" al chiamante semplicemente mettendo in pausa, non terminando, una funzione generatore.
È più facile immaginare un generatore come un'estremità di un tubo bidirezionale con un'estremità "sinistra" e un'estremità "destra"; questa pipe è il mezzo su cui vengono inviati i valori tra il generatore stesso e il corpo della funzione del generatore. Ciascuna estremità del tubo ha due operazioni push:, che invia un valore e si blocca fino a quando l'altra estremità del tubo tira il valore e non restituisce nulla; epull, che si blocca fino a quando l'altra estremità del tubo non spinge un valore e restituisce il valore spinto. In fase di esecuzione, l'esecuzione rimbalza avanti e indietro tra i contesti su entrambi i lati del tubo: ogni lato viene eseguito fino a quando non invia un valore all'altro lato, a quel punto si ferma, lascia correre l'altro lato e attende un valore in ritorna, a quel punto l'altro lato si ferma e riprende. In altre parole, ciascuna estremità del tubo scorre dal momento in cui riceve un valore al momento in cui invia un valore.
La pipe è funzionalmente simmetrica, ma - per convenzione sto definendo in questa risposta - l'estremità sinistra è disponibile solo all'interno del corpo della funzione del generatore ed è accessibile tramite la yieldparola chiave, mentre l'estremità destra è il generatore ed è accessibile tramite il sendfunzione del generatore . Come interfacce singolari alle loro rispettive estremità del tubo, yielde sendfanno il doppio dovere: entrambi spingono e tirano entrambi i valori da / verso le loro estremità del tubo, yieldspingendo verso destra e tirando verso sinistra mentre sendfa l'opposto. Questo doppio dovere è il punto cruciale della confusione che circonda la semantica di affermazioni simili x = yield y. Rottura yielde sendin due fasi esplicito push / pull renderà la loro semantica molto più chiaro:
- Supponiamo che
gsia il generatore. g.sendspinge un valore verso sinistra attraverso l'estremità destra del tubo.
- Esecuzione nel contesto di
gpause, consentendo l'esecuzione del corpo della funzione generatore.
- Il valore spinto da
g.sendviene tirato a sinistra da yielde ricevuto sull'estremità sinistra del tubo. In x = yield y, xviene assegnato al valore estratto.
- L'esecuzione continua all'interno del corpo della funzione generatore fino a raggiungere la riga successiva contenente
yield.
yieldspinge un valore verso destra attraverso l'estremità sinistra del tubo, di nuovo su g.send. In x = yield y, yviene spinto a destra attraverso il tubo.
- L'esecuzione all'interno del corpo della funzione generatore si interrompe, consentendo all'oscilloscopio esterno di continuare da dove era stato interrotto.
g.send riprende e tira il valore e lo restituisce all'utente.
- Alla
g.sendsuccessiva chiamata, tornare al passaggio 1.
Sebbene ciclica, questa procedura ha un inizio: quando g.send(None)- che è l' next(g)abbreviazione - viene chiamato per la prima volta (è illegale passare qualcosa di diverso rispetto Nonealla prima sendchiamata). E può avere una fine: quando non ci sono più yielddichiarazioni da raggiungere nel corpo della funzione generatore.
Vedi cosa rende yieldcosì speciale l' affermazione (o, più precisamente, i generatori)? A differenza della returnparola chiave misero , yieldè in grado di passare valori al suo chiamante e ricevere valori dal suo chiamante tutto senza terminare la funzione in cui vive! (Naturalmente, se si desidera terminare una funzione - o un generatore - è utile avere anche la returnparola chiave.) Quando yieldviene rilevata un'istruzione, la funzione generatore si interrompe e riprende da dove era rimasta spento dopo l'invio di un altro valore. Ed sendè solo l'interfaccia per comunicare con l'interno di una funzione del generatore dall'esterno.
Se vogliamo davvero rompere al massimo questa analogia push / pull / pipe, finiamo con il seguente pseudocodice che porta davvero a casa che, a parte i passaggi 1-5, yielde sendsono due facce dello stesso gettone :
right_end.push(None) # the first half of g.send; sending None is what starts a generator
right_end.pause()
left_end.start()
initial_value = left_end.pull()
if initial_value is not None: raise TypeError("can't send non-None value to a just-started generator")
left_end.do_stuff()
left_end.push(y) # the first half of yield
left_end.pause()
right_end.resume()
value1 = right_end.pull() # the second half of g.send
right_end.do_stuff()
right_end.push(value2) # the first half of g.send (again, but with a different value)
right_end.pause()
left_end.resume()
x = left_end.pull() # the second half of yield
goto 6
La trasformazione chiave è che abbiamo diviso x = yield ye value1 = g.send(value2)ciascuno in due affermazioni: left_end.push(y)e x = left_end.pull(); e value1 = right_end.pull()e right_end.push(value2). Esistono due casi speciali della yieldparola chiave: x = yielde yield y. Questi sono zuccheri sintattici, rispettivamente, per x = yield Nonee _ = yield y # discarding value.
Per dettagli specifici sull'ordine preciso in cui i valori vengono inviati attraverso la pipe, vedere di seguito.
Quello che segue è un modello concreto piuttosto lungo di quanto sopra. In primo luogo, occorre innanzitutto rilevare che per ogni generatore g, next(g)è esattamente equivalente g.send(None). Con questo in mente possiamo concentrarci solo su come sendfunziona e parlare solo dell'avanzamento del generatore send.
Supponiamo di avere
def f(y): # This is the "generator function" referenced above
while True:
x = yield y
y = x
g = f(1)
g.send(None) # yields 1
g.send(2) # yields 2
Ora, la definizione di fcirca desugar alla seguente funzione ordinaria (non generatrice):
def f(y):
bidirectional_pipe = BidirectionalPipe()
left_end = bidirectional_pipe.left_end
right_end = bidirectional_pipe.right_end
def impl():
initial_value = left_end.pull()
if initial_value is not None:
raise TypeError(
"can't send non-None value to a just-started generator"
)
while True:
left_end.push(y)
x = left_end.pull()
y = x
def send(value):
right_end.push(value)
return right_end.pull()
right_end.send = send
# This isn't real Python; normally, returning exits the function. But
# pretend that it's possible to return a value from a function and then
# continue execution -- this is exactly the problem that generators were
# designed to solve!
return right_end
impl()
In questa trasformazione di f:
- Abbiamo spostato l'implementazione in una funzione nidificata.
- Abbiamo creato una pipe bidirezionale a cui
left_endaccederà la funzione nidificata e che right_endverrà restituita e accessibile dall'ambito esterno - right_endè ciò che conosciamo come oggetto generatore.
- All'interno della funzione nidificata, la prima cosa che facciamo è controllare che
left_end.pull()è None, consumando un valore spinto nel processo.
- All'interno della funzione nidificata, l'istruzione
x = yield yè stata sostituita da due righe: left_end.push(y)e x = left_end.pull().
- Abbiamo definito la
sendfunzione per right_end, che è la controparte delle due righe con cui abbiamo sostituito l' x = yield yistruzione nel passaggio precedente.
In questo mondo fantastico in cui le funzioni possono continuare dopo il ritorno, gviene assegnato right_ende quindi impl()chiamato. Quindi, nel nostro esempio sopra, se seguissimo l'esecuzione riga per riga, ciò che accadrebbe sarebbe all'incirca il seguente:
left_end = bidirectional_pipe.left_end
right_end = bidirectional_pipe.right_end
y = 1 # from g = f(1)
# None pushed by first half of g.send(None)
right_end.push(None)
# The above push blocks, so the outer scope halts and lets `f` run until
# *it* blocks
# Receive the pushed value, None
initial_value = left_end.pull()
if initial_value is not None: # ok, `g` sent None
raise TypeError(
"can't send non-None value to a just-started generator"
)
left_end.push(y)
# The above line blocks, so `f` pauses and g.send picks up where it left off
# y, aka 1, is pulled by right_end and returned by `g.send(None)`
right_end.pull()
# Rinse and repeat
# 2 pushed by first half of g.send(2)
right_end.push(2)
# Once again the above blocks, so g.send (the outer scope) halts and `f` resumes
# Receive the pushed value, 2
x = left_end.pull()
y = x # y == x == 2
left_end.push(y)
# The above line blocks, so `f` pauses and g.send(2) picks up where it left off
# y, aka 2, is pulled by right_end and returned to the outer scope
right_end.pull()
x = left_end.pull()
# blocks until the next call to g.send
Questo si associa esattamente allo pseudocodice di 16 passaggi sopra.
Ci sono alcuni altri dettagli, come la modalità di propagazione degli errori e cosa succede quando si raggiunge la fine del generatore (il tubo è chiuso), ma ciò dovrebbe chiarire come funziona il flusso di controllo di base quando sendviene utilizzato.
Usando queste stesse regole di desugaring, diamo un'occhiata a due casi speciali:
def f1(x):
while True:
x = yield x
def f2(): # No parameter
while True:
x = yield x
Per la maggior parte desugar allo stesso modo f, le uniche differenze sono le modalità di yieldtrasformazione delle affermazioni:
def f1(x):
# ... set up pipe
def impl():
# ... check that initial sent value is None
while True:
left_end.push(x)
x = left_end.pull()
# ... set up right_end
def f2():
# ... set up pipe
def impl():
# ... check that initial sent value is None
while True:
left_end.push(x)
x = left_end.pull()
# ... set up right_end
Nel primo, il valore passato a f1viene inizialmente spinto (ceduto), quindi tutti i valori estratti (inviati) vengono spinti (ceduti) all'indietro. Nel secondo, xnon ha valore (ancora) quando arriva per la prima volta push, quindi UnboundLocalErrorviene sollevato.