In pratica, quali sono gli usi principali della nuova sintassi "yield from" in Python 3.3?


407

Sto facendo fatica ad avvolgere il mio cervello attorno al PEP 380 .

  1. Quali sono le situazioni in cui il "rendimento da" è utile?
  2. Qual è il classico caso d'uso?
  3. Perché viene confrontato con i micro-thread?

[ aggiornare ]

Ora capisco la causa delle mie difficoltà. Ho usato generatori, ma non ho mai usato coroutine (introdotte da PEP-342 ). Nonostante alcune somiglianze, generatori e coroutine sono fondamentalmente due concetti diversi. Comprendere le coroutine (non solo i generatori) è la chiave per comprendere la nuova sintassi.

Le coroutine IMHO sono la caratteristica più oscura di Python , la maggior parte dei libri lo rende inutile e poco interessante.

Grazie per le ottime risposte, ma un ringraziamento speciale a agf e al suo commento collegato alle presentazioni di David Beazley . David spacca.



7
Video della presentazione dabeaz.com/coroutines di David Beazley : youtube.com/watch?v=Z_OAlIhXziw
jcugat,

Risposte:


572

Prima di tutto togliiamo una cosa. La spiegazione che yield from gequivale a for v in g: yield v non inizia nemmeno a rendere giustizia a tutto ciò che yield fromriguarda. Perché, ammettiamolo, se tutto yield fromciò che fa è espandere il forciclo, non garantisce l'aggiunta yield fromal linguaggio e preclude l'implementazione di un sacco di nuove funzionalità in Python 2.x.

Quello che yield fromfa è 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 gpotrebbe 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 fromfarlo.

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 writerche 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 StopIterationquando il ciclo for è esaurito. Evidentemente, semplicemente for x in coro: yield xnon 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 writermaniglie a SpamExceptione 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 fromgestisce 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 fromgestisce in modo trasparente tutte le custodie angolari è davvero impressionante . yield fromfunziona magicamente e gestisce tutti quei casi.

Personalmente ritengo yield fromsia una scelta di parole chiave scadente perché non rende evidente la natura a due vie . Sono state proposte altre parole chiave (come delegatema 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 fromcome transparent two way channeltra il chiamante e il sottogeneratore.

Riferimenti:

  1. PEP 380 - Sintassi per delegare a un sottogeneratore (Ewing) [v3.3, 2009-02-13]
  2. PEP 342 - Coroutine tramite generatori avanzati (GvR, Eby) [v2.5, 2005-05-10]

3
@PraveenGollakota, nella seconda parte della tua domanda, Invio di dati a un generatore (coroutine) utilizzando il rendimento da - Parte 1 , cosa succede se hai più di coroutine a cui inoltrare l'articolo ricevuto? Come uno scenario di emittente o abbonato in cui fornisci più coroutine al wrapper nel tuo esempio e gli articoli dovrebbero essere inviati a tutti o sottoinsieme di essi?
Kevin Ghaboosi

3
@PraveenGollakota, Kudos per l'ottima risposta. I piccoli esempi mi permettono di provare le cose in sostituzione. Il collegamento al corso Dave Beazley è stato un vantaggio!
BiGYaN,

1
facendo except StopIteration: passINSIDE il while True:loop non è una rappresentazione accurata di yield from coro- che non è un loop infinito e dopo che coroè esaurito (cioè solleva StopIteration), writer_wrappereseguirà l'istruzione successiva. Dopo l'ultima affermazione, si alzerà automaticamente StopIterationcome qualsiasi generatore esausto ...
Aprillion

1
... quindi se writercontenuto for _ in range(4)invece di while True, quindi dopo la stampa >> 3si solleverebbe ANCHE da solo StopIteratione questo sarebbe gestito automaticamente da yield frome quindi writer_wrappersi solleverebbe automaticamente da solo StopIteratione poiché wrap.send(i)non è all'interno del tryblocco, sarebbe effettivamente sollevato a questo punto ( cioè traceback riporterà solo la linea con wrap.send(i), non nulla dall'interno del generatore)
Aprillion

3
Dopo aver letto " non inizia nemmeno a rendere giustizia ", so di essere arrivato alla risposta giusta. Grazie per l'ottima spiegazione!
Hot.PxL

89

Quali sono le situazioni in cui il "rendimento da" è utile?

Ogni situazione in cui hai un ciclo come questo:

for x in subgenerator:
  yield x

Come descrive il PEP, si tratta di un tentativo piuttosto ingenuo di usare il sottogeneratore, mancano diversi aspetti, in particolare la corretta gestione dei meccanismi .throw()/ .send()/ .close()introdotti da PEP 342 . Per farlo correttamente, è necessario un codice piuttosto complicato .

Qual è il classico caso d'uso?

Si consideri che si desidera estrarre informazioni da una struttura di dati ricorsiva. Diciamo che vogliamo ottenere tutti i nodi foglia in un albero:

def traverse_tree(node):
  if not node.children:
    yield node
  for child in node.children:
    yield from traverse_tree(child)

Ancora più importante è il fatto che fino al yield from, non esisteva un metodo semplice per il refactoring del codice del generatore. Supponiamo di avere un generatore (insensato) come questo:

def get_list_values(lst):
  for item in lst:
    yield int(item)
  for item in lst:
    yield str(item)
  for item in lst:
    yield float(item)

Ora decidi di scomporre questi loop in generatori separati. Senza yield from, questo è brutto, fino al punto in cui penserai due volte se davvero vuoi farlo. Con yield from, in realtà è bello guardare:

def get_list_values(lst):
  for sub in [get_list_values_as_int, 
              get_list_values_as_str, 
              get_list_values_as_float]:
    yield from sub(lst)

Perché viene confrontato con i micro-thread?

Penso che ciò di cui questa sezione del PEP stia parlando sia che ogni generatore ha il suo contesto di esecuzione isolato. Insieme al fatto che l'esecuzione viene commutata tra il generatore-iteratore e il chiamante usando yielde __next__(), rispettivamente, questo è simile ai thread, in cui il sistema operativo cambia il thread in esecuzione di volta in volta, insieme al contesto di esecuzione (stack, registri, ...).

L'effetto di questo è anche comparabile: sia il generatore-iteratore che il chiamante avanzano nel loro stato di esecuzione contemporaneamente, le loro esecuzioni sono intercalate. Ad esempio, se il generatore esegue una sorta di calcolo e il chiamante stampa i risultati, vedrai i risultati non appena saranno disponibili. Questa è una forma di concorrenza.

Quell'analogia non è nulla di specifico yield from, tuttavia, è piuttosto una proprietà generale dei generatori in Python.


I generatori di refactoring sono dolorosi oggi.
Josh Lee

1
Tendo a usare molto gli itertools per i generatori di refactoring (cose come itertools.chain), non è un grosso problema. Mi piace cedere, ma non riesco ancora a vedere quanto sia rivoluzionario. Probabilmente lo è, dal momento che Guido è pazzo per questo, ma mi devo perdere il quadro generale. Immagino sia fantastico per send () poiché è difficile da refactoring, ma non lo uso abbastanza spesso.
e-satis

Suppongo che get_list_values_as_xxxsiano generatori semplici con una sola riga for x in input_param: yield int(x)e le altre due rispettivamente con strefloat
madtyn

@NiklasB. ri "estrarre informazioni da una struttura di dati ricorsiva". Sto solo entrando in Py per i dati. Potresti prendere un colpo a questa Q ?
alancalvitti,

33

Ovunque si invoca un generatore all'interno di un generatore hai bisogno di una "pompa" di ri- yieldi valori: for v in inner_generator: yield v. Come sottolinea il PEP, ci sono sottili complessità che la maggior parte delle persone ignora. Il controllo di flusso non locale come throw()è un esempio fornito nel PEP. La nuova sintassi yield from inner_generatorviene utilizzata ovunque avresti scritto forprima il ciclo esplicito . Non è solo zucchero sintattico, però: gestisce tutti i casi angolari che vengono ignorati dal forciclo. Essere "zuccherati" incoraggia le persone ad usarlo e quindi ottenere i comportamenti giusti.

Questo messaggio nel thread di discussione parla di queste complessità:

Con le funzionalità aggiuntive del generatore introdotte da PEP 342, questo non è più il caso: come descritto in PEP di Greg, la semplice iterazione non supporta send () e throw () correttamente. La ginnastica necessaria per supportare send () e throw () in realtà non è così complessa quando li scomponi, ma non sono nemmeno banali.

Non posso parlare di un confronto con i micro-thread, se non quello di osservare che i generatori sono un tipo di paralellismo. Puoi considerare il generatore sospeso come un thread che invia valori tramite yieldun thread consumer. L'implementazione effettiva potrebbe non essere nulla del genere (e l'implementazione effettiva è ovviamente di grande interesse per gli sviluppatori Python), ma ciò non riguarda gli utenti.

La nuova yield fromsintassi non aggiunge alcuna funzionalità aggiuntiva alla lingua in termini di thread, ma semplifica l'utilizzo corretto delle funzioni esistenti. O più precisamente, per un consumatore alle prime armi di un generatore interno complesso, scritto da un esperto, è più semplice passare attraverso quel generatore senza rompere nessuna delle sue complesse caratteristiche.


23

Un breve esempio ti aiuterà a capire uno dei yield fromcasi d'uso: ottenere valore da un altro generatore

def flatten(sequence):
    """flatten a multi level list or something
    >>> list(flatten([1, [2], 3]))
    [1, 2, 3]
    >>> list(flatten([1, [2], [3, [4]]]))
    [1, 2, 3, 4]
    """
    for element in sequence:
        if hasattr(element, '__iter__'):
            yield from flatten(element)
        else:
            yield element

print(list(flatten([1, [2], [3, [4]]])))

2
Volevo solo suggerire che la stampa alla fine sarebbe stata un po 'più bella senza la conversione in un elenco -print(*flatten([1, [2], [3, [4]]]))
yoniLavi,

6

yield from fondamentalmente incatena gli iteratori in modo efficiente:

# chain from itertools:
def chain(*iters):
    for it in iters:
        for item in it:
            yield item

# with the new keyword
def chain(*iters):
    for it in iters:
        yield from it

Come puoi vedere, rimuove un loop Python puro. È praticamente tutto ciò che fa, ma concatenare gli iteratori è un modello abbastanza comune in Python.

I thread sono fondamentalmente una funzione che ti consente di saltare fuori dalle funzioni in punti completamente casuali e tornare allo stato di un'altra funzione. Il supervisore del thread lo fa molto spesso, quindi il programma sembra eseguire tutte queste funzioni contemporaneamente. Il problema è che i punti sono casuali, quindi è necessario utilizzare il blocco per impedire al supervisore di interrompere la funzione in un punto problematico.

I generatori sono abbastanza simili ai thread in questo senso: ti permettono di specificare punti specifici (ogni volta che li yield) dove puoi saltare dentro e fuori. Se usati in questo modo, i generatori sono chiamati coroutine.

Leggi questo eccellente tutorial sulle coroutine in Python per maggiori dettagli


10
Questa risposta è fuorviante perché elude la caratteristica saliente di "cedere da", come menzionato sopra: send () e throw () support.
Justin W,

2
@Justin W: Immagino che qualunque cosa tu abbia letto prima sia effettivamente fuorviante, perché non hai capito il punto che throw()/send()/close()sono yieldcaratteristiche che yield fromovviamente devono implementare correttamente in quanto si suppone che semplifichino il codice. Tali banalità non hanno nulla a che fare con l'uso.
Jochen Ritzel,

5
Stai contestando la risposta di Ben Jackson sopra? La mia lettura della tua risposta è che è essenzialmente lo zucchero sintattico che segue la trasformazione del codice che hai fornito. La risposta di Ben Jackson respinge specificamente tale affermazione.
Justin W,

@JochenRitzel Non hai mai bisogno di scrivere la tua chainfunzione perché itertools.chainesiste già. Usa yield from itertools.chain(*iters).
Acumenus,

5

Nell'uso applicato per la coroutine asincrona IO , yield fromha un comportamento simile a quello awaitdi una funzione coroutine . Entrambi sono usati per sospendere l'esecuzione del coroutine.

Per Asyncio, se non è necessario supportare una versione precedente di Python (ovvero> 3.5), async def/ awaitè la sintassi consigliata per definire una coroutine. Quindi yield fromnon è più necessario in un coroutine.

Ma in generale al di fuori di asyncio, yield from <sub-generator>ha ancora qualche altro uso nell'iterare il sottogeneritore come menzionato nella risposta precedente.


1

Questo codice definisce una funzione che fixed_sum_digitsrestituisce un generatore che elenca tutti i numeri di sei cifre in modo tale che la somma delle cifre sia 20.

def iter_fun(sum, deepness, myString, Total):
    if deepness == 0:
        if sum == Total:
            yield myString
    else:  
        for i in range(min(10, Total - sum + 1)):
            yield from iter_fun(sum + i,deepness - 1,myString + str(i),Total)

def fixed_sum_digits(digits, Tot):
    return iter_fun(0,digits,"",Tot) 

Prova a scriverlo senza yield from. Se trovi un modo efficace per farlo, fammelo sapere.

Penso che per casi come questo: visitare gli alberi, yield fromrenda il codice più semplice e più pulito.


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.