Prima di tutto togliiamo una cosa. La spiegazione che yield from g
equivale a for v in g: yield v
non inizia nemmeno a rendere giustizia a tutto ciò che yield from
riguarda. Perché, ammettiamolo, se tutto yield from
ciò che fa è espandere il for
ciclo, non garantisce l'aggiunta yield from
al linguaggio e preclude l'implementazione di un sacco di nuove funzionalità in Python 2.x.
Quello che yield from
fa è stabilire una connessione bidirezionale trasparente tra il chiamante e il sottogeneratore :
La connessione è "trasparente", nel senso che propagherà anche tutto correttamente, non solo gli elementi generati (ad esempio, le eccezioni vengono propagate).
La connessione è "bidirezionale", nel senso che i dati possono essere sia inviato da e ad un generatore.
( Se stessimo parlando di TCP, yield from g
potrebbe significare "ora disconnettere temporaneamente il socket del mio client e ricollegarlo a questo altro socket del server". )
A proposito, se non sei sicuro di cosa significhi anche l' invio di dati a un generatore , devi eliminare tutto e leggere prima le coroutine : sono molto utili (contrastale con le subroutine ), ma purtroppo meno conosciute in Python. Il Curious Course on Coroutines di Dave Beazley è un ottimo inizio. Leggi le diapositive 24-33 per un innesco rapido.
Leggere i dati da un generatore usando il rendimento da
def reader():
"""A generator that fakes a read from a file, socket, etc."""
for i in range(4):
yield '<< %s' % i
def reader_wrapper(g):
# Manually iterate over data produced by reader
for v in g:
yield v
wrap = reader_wrapper(reader())
for i in wrap:
print(i)
# Result
<< 0
<< 1
<< 2
<< 3
Invece di iterare manualmente reader()
, possiamo proprio yield from
farlo.
def reader_wrapper(g):
yield from g
Funziona e abbiamo eliminato una riga di codice. E probabilmente l'intento è un po 'più chiaro (o meno). Ma niente cambia la vita.
Invio di dati a un generatore (coroutine) utilizzando il rendimento da - Parte 1
Ora facciamo qualcosa di più interessante. Creiamo un coroutine chiamato writer
che accetta i dati inviati e scrive su un socket, fd, ecc.
def writer():
"""A coroutine that writes data *sent* to it to fd, socket, etc."""
while True:
w = (yield)
print('>> ', w)
Ora la domanda è: come dovrebbe la funzione wrapper gestire l'invio di dati al writer, in modo che tutti i dati inviati al wrapper vengano inviati in modo trasparente a writer()
?
def writer_wrapper(coro):
# TBD
pass
w = writer()
wrap = writer_wrapper(w)
wrap.send(None) # "prime" the coroutine
for i in range(4):
wrap.send(i)
# Expected result
>> 0
>> 1
>> 2
>> 3
Il wrapper deve accettare i dati che gli vengono inviati (ovviamente) e deve anche gestire StopIteration
quando il ciclo for è esaurito. Evidentemente, semplicemente for x in coro: yield x
non lo farà. Ecco una versione che funziona.
def writer_wrapper(coro):
coro.send(None) # prime the coro
while True:
try:
x = (yield) # Capture the value that's sent
coro.send(x) # and pass it to the writer
except StopIteration:
pass
Oppure potremmo farlo.
def writer_wrapper(coro):
yield from coro
Questo salva 6 righe di codice, lo rende molto più leggibile e funziona. Magia!
L'invio di dati a un generatore genera da - Parte 2 - Gestione delle eccezioni
Rendiamolo più complicato. Cosa succede se il nostro scrittore deve gestire le eccezioni? Diciamo che le writer
maniglie a SpamException
e stampa ***
se ne incontra una.
class SpamException(Exception):
pass
def writer():
while True:
try:
w = (yield)
except SpamException:
print('***')
else:
print('>> ', w)
E se non cambiassimo writer_wrapper
? Funziona? Proviamo
# writer_wrapper same as above
w = writer()
wrap = writer_wrapper(w)
wrap.send(None) # "prime" the coroutine
for i in [0, 1, 2, 'spam', 4]:
if i == 'spam':
wrap.throw(SpamException)
else:
wrap.send(i)
# Expected Result
>> 0
>> 1
>> 2
***
>> 4
# Actual Result
>> 0
>> 1
>> 2
Traceback (most recent call last):
... redacted ...
File ... in writer_wrapper
x = (yield)
__main__.SpamException
Ehm, non funziona perché x = (yield)
solleva solo l'eccezione e tutto si ferma. Facciamolo funzionare, ma gestendo manualmente le eccezioni e inviandole o gettandole nel sotto-generatore ( writer
)
def writer_wrapper(coro):
"""Works. Manually catches exceptions and throws them"""
coro.send(None) # prime the coro
while True:
try:
try:
x = (yield)
except Exception as e: # This catches the SpamException
coro.throw(e)
else:
coro.send(x)
except StopIteration:
pass
Questo funziona
# Result
>> 0
>> 1
>> 2
***
>> 4
Ma anche questo!
def writer_wrapper(coro):
yield from coro
I yield from
gestisce in modo trasparente l'invio dei valori o gettando valori nel sub-generatore.
Questo comunque non copre tutti i casi angolari. Cosa succede se il generatore esterno è chiuso? Che dire del caso in cui il sottogeneritore restituisce un valore (sì, in Python 3.3+, i generatori possono restituire valori), come deve essere propagato il valore restituito? Che yield from
gestisce in modo trasparente tutte le custodie angolari è davvero impressionante . yield from
funziona magicamente e gestisce tutti quei casi.
Personalmente ritengo yield from
sia una scelta di parole chiave scadente perché non rende evidente la natura a due vie . Sono state proposte altre parole chiave (come delegate
ma sono state respinte perché l'aggiunta di una nuova parola chiave alla lingua è molto più difficile rispetto alla combinazione di quelle esistenti.
In sintesi, è meglio pensare yield from
come transparent two way channel
tra il chiamante e il sottogeneratore.
Riferimenti:
- PEP 380 - Sintassi per delegare a un sottogeneratore (Ewing) [v3.3, 2009-02-13]
- PEP 342 - Coroutine tramite generatori avanzati (GvR, Eby) [v2.5, 2005-05-10]