Najpierw usuńmy jedno. Wyjaśnienie, które yield from g
jest równoważne z for v in g: yield v
tym, nie zaczyna nawet oddawać sprawiedliwości temu, o co w tym yield from
wszystkim chodzi. Ponieważ, spójrzmy prawdzie w oczy, jeśli wszystko yield from
zrobi, to rozwinięcie for
pętli, nie gwarantuje to dodania yield from
do języka i wyklucza wprowadzenie całej gamy nowych funkcji w Pythonie 2.x.
Co yield from
to jest ustanawia przejrzyste dwukierunkowe połączenie między dzwoniącym a pod-generatorem :
Połączenie jest „przezroczyste” w tym sensie, że propaguje również wszystko poprawnie, nie tylko generowane elementy (np. Propagowane są wyjątki).
Połączenie jest „dwukierunkowe” w tym sensie, że dane mogą być wysyłane zarówno z generatora, jak i do niego.
( Gdybyśmy mówili o TCP, yield from g
może to oznaczać „teraz tymczasowo odłącz gniazdo mojego klienta i podłącz je ponownie do tego drugiego gniazda serwera” ).
BTW, jeśli nie jesteś pewien, co w ogóle oznacza wysyłanie danych do generatora , musisz porzucić wszystko i najpierw przeczytać o coroutines - są one bardzo przydatne (w przeciwieństwie do podprogramów ), ale niestety mniej znane w Pythonie. Ciekawy kurs Dave'a Beazleya na coroutines to doskonały początek. Przeczytaj slajdy 24-33, aby uzyskać szybki podkład.
Odczyt danych z generatora przy użyciu wydajności z
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
Zamiast ręcznie iterować reader()
, możemy to yield from
zrobić.
def reader_wrapper(g):
yield from g
To działa i wyeliminowaliśmy jeden wiersz kodu. I prawdopodobnie cel jest nieco jaśniejszy (lub nie). Ale nic nie zmienia życia.
Przesyłanie danych do generatora (coroutine) z wykorzystaniem wydajności z - Część 1
Zróbmy teraz coś ciekawszego. Stwórzmy nazwaną coroutine, writer
która akceptuje wysyłane do niej dane i zapisuje w gnieździe, fd itp.
def writer():
"""A coroutine that writes data *sent* to it to fd, socket, etc."""
while True:
w = (yield)
print('>> ', w)
Teraz pytanie brzmi: w jaki sposób funkcja opakowania powinna obsługiwać wysyłanie danych do programu piszącego, aby wszelkie dane wysyłane do opakowania były w sposób przezroczysty przesyłane do 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
Opakowanie musi zaakceptować dane, które są do niego wysyłane (oczywiście) i powinno również obsłużyć StopIteration
moment wyczerpania pętli for. Najwyraźniej samo robienie for x in coro: yield x
tego nie da. Oto wersja, która działa.
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
Lub możemy to zrobić.
def writer_wrapper(coro):
yield from coro
Oszczędza to 6 linii kodu, czyni go znacznie bardziej czytelnym i po prostu działa. Magia!
Przesyłanie danych do generatora generuje z - Część 2 - Obsługa wyjątków
Zróbmy to bardziej skomplikowanym. Co jeśli nasz pisarz musi poradzić sobie z wyjątkami? Powiedzmy, że writer
uchwyty SpamException
a drukuje, ***
jeśli je napotka.
class SpamException(Exception):
pass
def writer():
while True:
try:
w = (yield)
except SpamException:
print('***')
else:
print('>> ', w)
Co jeśli się nie zmienimy writer_wrapper
? Czy to działa? Spróbujmy
# 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
Nie działa, bo x = (yield)
podnosi wyjątek i wszystko się kończy. Sprawmy, by działało, ale ręcznie obsługujemy wyjątki i wysyłamy je lub wrzucamy do pod-generatora ( 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
To działa.
# Result
>> 0
>> 1
>> 2
***
>> 4
Ale tak też się dzieje!
def writer_wrapper(coro):
yield from coro
W yield from
przejrzysty uchwyty wysyłania wartości lub rzucanie wartości do sub-generator.
Nie dotyczy to jednak wszystkich przypadków narożnych. Co się stanie, jeśli zewnętrzny generator zostanie zamknięty? Co z przypadkiem, gdy pod-generator zwraca wartość (tak, w Pythonie 3.3+, generatory mogą zwracać wartości), w jaki sposób należy propagować zwracaną wartość? To, że w yield from
przejrzysty sposób obsługuje wszystkie skrzynki narożne, jest naprawdę imponujące . yield from
po prostu magicznie działa i obsługuje wszystkie te przypadki.
Osobiście uważam, że yield from
jest to zły wybór słów kluczowych, ponieważ nie uwidacznia to dwukierunkowej natury. Zaproponowano inne słowa kluczowe (jak, delegate
ale zostały odrzucone, ponieważ dodanie nowego słowa kluczowego do języka jest znacznie trudniejsze niż połączenie istniejących.
Podsumowując, najlepiej jest traktować to yield from
jako połączenie transparent two way channel
między dzwoniącym a sub-generatorem.
Bibliografia:
- PEP 380 - Składnia delegowania do pod-generatora (Ewing) [v3.3, 2009-02-13]
- PEP 342 - Coroutines via Enhanced Generators (GvR, Eby) [v2.5, 2005-05-10]