Wat zijn in de praktijk de belangrijkste toepassingen van de nieuwe “opbrengst van”-syntaxis in Python 3.3?

Ik vind het moeilijk om mijn brein rond PEP 380te wikkelen .

  1. In welke situaties is ‘opbrengst van’ nuttig?
  2. Wat is het klassieke gebruik?
  3. Waarom wordt het vergeleken met microthreads?

[ bijwerken]

Nu begrijp ik de oorzaak van mijn problemen. Ik heb generatoren gebruikt, maar nooit echt coroutines gebruikt (geïntroduceerd door PEP-342). Ondanks enkele overeenkomsten zijn generatoren en coroutines in feite twee verschillende concepten. Coroutines begrijpen (niet alleen generatoren) is de sleutel tot het begrijpen van de nieuwe syntaxis.

IMHO coroutines zijn de meest obscure Python-functie, in de meeste boeken ziet het er nutteloos en oninteressant uit.

Bedankt voor de geweldige antwoorden, maar speciale dank aan agfen zijn reactie die linkt naar David Beazley-presentaties. David rockt.


Antwoord 1, autoriteit 100%

Laten we eerst één ding uit de weg ruimen. De uitleg dat yield from ggelijk is aan for v in g: yield vbegint zelfs geen recht te doenaan wat yield fromgaat helemaal over. Want laten we eerlijk zijn, als de yield fromalleen de for-lus is, dan is het niet gerechtvaardigd om yield fromaan de taal toe te voegen en voorkomen dat een hele reeks nieuwe functies in Python 2.x worden geïmplementeerd.

Wat yield fromdoet, is dat het een transparante bidirectionele verbinding tot stand brengt tussen de beller en de subgenerator:

  • De verbinding is “transparant” in die zin dat het ook alles correct zal verspreiden, niet alleen de elementen die worden gegenereerd (bijvoorbeeld uitzonderingen zijn gepropageerd).

  • De verbinding is “bidirectioneel” in de zin dat gegevens beide kunnen worden verzonden van en naar een generator.

(Als we het hebben over TCP, yield from gzou kunnen betekenen “, descontact van mijn cliënt nu tijdelijk loskoppelen en opnieuw aansluiten op deze andere servercontactdoos”. )

BTW, als u niet zeker weet wat gegevens naar een generator verzenden, moet u zelfs alles laten vallen en lezen over Coroutines First – ze zijn erg handig ( Contrasteer ze met Subroutines ), maar helaas, minder bekend in Python. DAVE BEAZLEY’S CURIOUS CURSUS OP COROUTINES is een uitstekende start. Lees dia’s 24-33 voor een snelle primer.

Gegevens lezen van een generator met behulp van opbrengst van

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

In plaats van handmatig herhalen over de reader(), kunnen we gewoon yield fromIT.

def reader_wrapper(g):
    yield from g

die werkt, en we hebben één regel code geëlimineerd. En waarschijnlijk is de intentie een beetje duidelijker (of niet). Maar niets dat verandert.

Gegevens verzenden naar een generator (coroutine) met behulp van opbrengst uit – Deel 1

Laten we nu iets interessanters doen. Laten we een coroutine maken met de naam writerdie de verzonden gegevens accepteert en naar een socket, fd, enz. schrijft.

def writer():
    """A coroutine that writes data *sent* to it to fd, socket, etc."""
    while True:
        w = (yield)
        print('>> ', w)

De vraag is nu, hoe moet de wrapper-functie omgaan met het verzenden van gegevens naar de schrijver, zodat alle gegevens die naar de wrapper worden verzonden, transparantnaar de 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

De wrapper moet de gegevens accepterendie ernaartoe worden verzonden (uiteraard) en moet ook de StopIterationafhandelen wanneer de for-lus is uitgeput. Blijkbaar is het niet voldoende om for x in coro: yield xte doen. Hier is een versie die werkt.

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

Of we zouden dit kunnen doen.

def writer_wrapper(coro):
    yield from coro

Dat scheelt 6 regels code, maakt het veel leesbaarder en het werkt gewoon. Magie!

Het verzenden van gegevens naar een generatoropbrengst van – Deel 2 – Afhandeling van uitzonderingen

Laten we het ingewikkelder maken. Wat als onze schrijver uitzonderingen moet behandelen? Laten we zeggen dat de writereen SpamExceptionafhandelt en ***afdrukt als hij er een tegenkomt.

class SpamException(Exception):
    pass
def writer():
    while True:
        try:
            w = (yield)
        except SpamException:
            print('***')
        else:
            print('>> ', w)

Wat als we writer_wrapperniet wijzigen? Werkt het? Laten we proberen

# 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

Eh, het werkt niet omdat x = (yield)alleen de uitzondering verhoogt en alles tot stilstand komt. Laten we het laten werken, maar uitzonderingen handmatig afhandelen en verzenden of in de subgenerator gooien (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

Dit werkt.

# Result
>>  0
>>  1
>>  2
***
>>  4

Maar dit geldt ook!

def writer_wrapper(coro):
    yield from coro

De yield fromzorgt op transparante wijze voor het verzenden van de waarden of het gooien van waarden naar de subgenerator.

Dit dekt echter nog steeds niet alle hoekgevallen. Wat gebeurt er als de buitenste generator gesloten is? Hoe zit het met het geval dat de subgenerator een waarde retourneert (ja, in Python 3.3+ kunnen generatoren waarden retourneren), hoe moet de geretourneerde waarde worden gepropageerd? Dat yield fromhandelt transparant alle hoekgevallen af is echt indrukwekkend. yield fromwerkt gewoon magisch en behandelt al die gevallen.

Persoonlijk vind ik yield fromeen slechte zoekwoordkeuze, omdat het de tweerichtingsverkeerniet duidelijk maakt. Er zijn andere zoekwoorden voorgesteld (zoals delegate), maar deze zijn afgewezen omdat het toevoegen van een nieuw zoekwoord aan de taal veel moeilijker is dan het combineren van bestaande.

Samengevat is het het beste om yield fromte zien als een transparent two way channeltussen de beller en de subgenerator.

Referenties:

  1. PEP 380– Syntaxis voor delegeren aan een subgenerator (Ewing ) [v3.3, 13-02-2009]
  2. PEP 342
    Coroutines via verbeterde generatoren (GVR, EBY) [V2.5, 2005-05-10]

2, Autoriteit 14%

Wat zijn de situaties waarin “opbrengst van” nuttig is?

Elke situatie waarin je een lus hebt, zoals deze:

for x in subgenerator:
  yield x

Zoals de PEP beschrijft, is dit een nogal naïeve poging om de subgenerator te gebruiken, het ontbreekt verschillende aspecten, met name de juiste afhandeling van de .throw()/ .send()/ .close()mechanismen geïntroduceerd door pep 342 . Om dit goed te doen, is nogal gecompliceerd -code nodig.

Wat is de klassieke behuizing?

Overweeg dat u informatie wilt extraheren van een recursieve gegevensstructuur. Laten we zeggen dat we alle bladknopen in een boom willen krijgen:

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

Nog belangrijker is het feit dat tot de yield from, er geen eenvoudige methode voor het refactoreren van de generatorcode. Stel dat je een (zinloze) generator als volgt hebt:

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)

Nu besluit u deze lussen in afzonderlijke generatoren uit te voeren. Zonder yield fromis dit lelijk, tot het punt waar u twee keer denkt of u het eigenlijk wilt doen. Met yield from, het is eigenlijk leuk om naar te kijken:

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)

Waarom is het in vergelijking met microdraden?

Ik denk wat Dit gedeelte in de pep heeft het over dat elke generator heeft zijn eigen geïsoleerde uitvoeringscontext. Samen met het feit dat uitvoering wordt geschakeld tussen de generator-iterator en de beller met behulp van yielden __next__()respectievelijk, is dit vergelijkbaar met threads, waar het besturingssysteem schakelt De uitvoerende draad van tijd tot tijd, samen met de uitvoeringscontext (stapel, registers, …).

Het effect hiervan is ook vergelijkbaar: zowel de generator-iterator als de beller vordert in hun uitvoeringsstatus tegelijkertijd, hun executies zijn onderling verborgen. Als de generator bijvoorbeeld een soort bespreking doet en de beller afdrukt de resultaten, ziet u de resultaten zodra ze beschikbaar zijn. Dit is een vorm van concurrency.

Die analogie is niets specifieks voor yield from, maar het is nogal een algemene eigenschap van generatoren in Python.


3, Autoriteit 5%

Waar je ook een generator aanroept vanuit een generator, je hebt een “pomp” nodig om de waarden: yieldopnieuw te for v in inner_generator: yield v. Zoals de PEP aangeeft, zijn er subtiele complexiteiten die de meeste mensen negeren. Niet-lokale flow-control zoals throw()is een voorbeeld dat in de PEP wordt gegeven. De nieuwe syntaxis yield from inner_generatorwordt overal gebruikt waar je eerder de expliciete for-lus zou hebben geschreven. Het is echter niet alleen syntactische suiker: het behandelt alle hoekgevallen die worden genegeerd door de for-lus. “suikerachtig” zijn moedigt mensen aan om het te gebruiken en zo het juiste gedrag te krijgen.

Dit bericht in de discussiethreadgaat over deze complexiteiten:

Met de extra generatorfuncties die zijn geïntroduceerd door PEP 342, is dat geen
langer het geval: zoals beschreven in Greg’s PEP, eenvoudige iteratie niet
ondersteuning send() en throw() correct. De gymnastiek die nodig is om te ondersteunen
send() en throw() zijn eigenlijk niet zo ingewikkeld als je ze breekt
naar beneden, maar ze zijn ook niet triviaal.

Ik kan niet spreken over een vergelijkingmet micro-threads, behalve om te constateren dat generatoren een soort parallellisme zijn. Je kunt de opgeschorte generator beschouwen als een thread die waarden via yieldnaar een consumententhread stuurt. De daadwerkelijke implementatie is misschien niet zo (en de daadwerkelijke implementatie is natuurlijk van groot belang voor de Python-ontwikkelaars), maar dit gaat de gebruikers niet aan.

De nieuwe yield fromSyntaxis voegt geen extra mogelijkheden toe aan de taal in termen van threading, het maakt het gewoon gemakkelijker om bestaande functies correct te gebruiken. Of meer juist het maakt het gemakkelijker voor een beginnende consument van een complexe innerlijke generator geschreven door een -deskundige om door die generator door te gaan zonder een van de complexe kenmerken te breken.


4, Autoriteit 4%

Een kort voorbeeld helpt u om een ​​van yield from‘S GEBRUIK CASE: Krijg waarde van een andere generator

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

5

in toegepast gebruik voor de asynchrone io coroutine , yield fromheeft een vergelijkbaar gedrag als awaitin een Coroutine-functie . Beide worden gebruikt om de uitvoering van Coroutine op te schorten.

Voor Asyncio, als het niet nodig is om een oudere Python-versie (d.w.z. >3.5) te ondersteunen, is async def/awaitde aanbevolen syntaxis om een coroutine te definiëren. Dus yield fromis niet langer nodig in een coroutine.

Maar in het algemeen buiten asyncio, heeft yield from <sub-generator>nog een ander gebruik bij het herhalen van de subgeneratorzoals vermeld in het eerdere antwoord.


Antwoord 6

yield fromketent iterators in feite op een efficiënte manier:

# 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

Zoals je kunt zien, wordt één pure Python-lus verwijderd. Dat is zo’n beetje alles wat het doet, maar het koppelen van iterators is een vrij algemeen patroon in Python.

Draden zijn in feite een functie waarmee u op volledig willekeurige punten uit functies kunt springen en terug kunt springen naar de status van een andere functie. De thread-supervisor doet dit heel vaak, dus het lijkt erop dat het programma al deze functies tegelijkertijd uitvoert. Het probleem is dat de punten willekeurig zijn, dus je moet vergrendeling gebruiken om te voorkomen dat de supervisor de functie op een problematisch punt stopt.

Generatoren zijn in die zin vrij gelijkaardig aan draden: ze stellen u in staat specifieke punten op te geven (telkens wanneer zij yield) waar u in en uit kunt springen. Bij deze manier worden generatoren Coroutines genoemd.

Lees deze uitstekende tutorials over coroutines in Python voor meer informatie


7

Deze code definieert een functie fixed_sum_digitsTerugkeer van een generator die alle zes cijfersnummers opsommen, zodanig dat de som van cijfers 20 is.

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) 

Probeer het te schrijven zonder yield from. Als je een effectieve manier vindt om het te doen, laat het me weten.

Ik denk dat dat voor zaken zoals deze: een bezoek aan bomen, yield frommaakt de code eenvoudiger en reiniger.


8

yieldlevert een enkele waarde op in de verzameling.

yield fromlevert de collectie op in de collectie en maakt het plat.

Controleer dit voorbeeld:

def yieldOnly():
    yield "A"
    yield "B"
    yield "C"
def yieldFrom():
    for i in [1, 2, 3]:
        yield from yieldOnly()
test = yieldFrom()
for i in test:
print(i)

In console ziet u:

A
B
C
A
B
C
A
B
C

Other episodes