Ik vind het moeilijk om mijn brein rond PEP 380te wikkelen .
- In welke situaties is ‘opbrengst van’ nuttig?
- Wat is het klassieke gebruik?
- 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 g
gelijk is aan for v in g: yield v
begint zelfs geen recht te doenaan wat yield from
gaat helemaal over. Want laten we eerlijk zijn, als de yield from
alleen de for
-lus is, dan is het niet gerechtvaardigd om yield from
aan de taal toe te voegen en voorkomen dat een hele reeks nieuwe functies in Python 2.x worden geïmplementeerd.
Wat yield from
doet, 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 g
zou 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 from
IT.
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 writer
die 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 StopIteration
afhandelen wanneer de for-lus is uitgeput. Blijkbaar is het niet voldoende om for x in coro: yield x
te 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 writer
een SpamException
afhandelt 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_wrapper
niet 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 from
zorgt 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 from
handelt transparant alle hoekgevallen af is echt indrukwekkend. yield from
werkt gewoon magisch en behandelt al die gevallen.
Persoonlijk vind ik yield from
een 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 from
te zien als een transparent two way channel
tussen de beller en de subgenerator.
Referenties:
- PEP 380– Syntaxis voor delegeren aan een subgenerator (Ewing ) [v3.3, 13-02-2009]
- 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 from
is 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 yield
en __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: yield
opnieuw 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_generator
wordt 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 yield
naar 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 from
Syntaxis 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 from
heeft een vergelijkbaar gedrag als await
in een Coroutine-functie . Beide worden gebruikt om de uitvoering van Coroutine op te schorten.
-
yield from
wordt gebruikt door de Generator-gebaseerde Coroutine . -
await
wordt gebruikt voorasync def
coroutine. (sinds Python 3.5+)
Voor Asyncio, als het niet nodig is om een oudere Python-versie (d.w.z. >3.5) te ondersteunen, is async def
/await
de aanbevolen syntaxis om een coroutine te definiëren. Dus yield from
is 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 from
ketent 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_digits
Terugkeer 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 from
maakt de code eenvoudiger en reiniger.
8
yield
levert een enkele waarde op in de verzameling.
yield from
levert 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