Давайте сначала уберем одну вещь с пути.Объяснение, что yield from g
эквивалентно for v in g: yield v
, даже не начинает отдавать должное тому, что такое yield from
.Потому что, давайте посмотрим правде в глаза, если все, что yield from
делает, это расширяет цикл for
, то это не гарантирует добавление yield from
к языку и препятствует реализации целого ряда новых функций в Python 2.x.
Что yield from
делает, это устанавливает прозрачное двунаправленное соединение между вызывающим абонентом и вспомогательным генератором :
Соединение является «прозрачным» в том смысле, что оно будет также правильно распространять все, а не только генерируемые элементы (например, распространяются исключения).
Соединение является «двунаправленным» вчувствую, что данные могут быть отправлены из и в генератор.
( Если мы говорили о TCP, yield from g
может означать «теперь временно отключите сокет моего клиента и подключите его к этому другому серверному сокету». )
Кстати, если вы не уверены, что отправляет данные в генератор дажезначит, вам нужно все бросить исначала прочитайте о сопрограммах - они очень полезны (противопоставьте их подпрограммам ), но, к сожалению, менее известны в Python. Любопытный курс Дейва Бизли по занятиям - отличное начало. Считайте слайды 24-33 для быстрого праймера.
Чтение данных из генератора, используя выход из
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
Вместо ручной итерации по reader()
, мы можемпросто yield from
it.
def reader_wrapper(g):
yield from g
Это работает, и мы исключили одну строку кода.И, вероятно, намерение немного яснее (или нет).Но ничего не меняет жизнь.
Отправка данных в генератор (сопрограмму) с использованием yield из - Part 1
Теперь давайте сделаем что-нибудь более интересное.Давайте создадим сопрограмму с именем writer
, которая принимает отправленные ей данные и записывает данные в сокет, fd и т. Д.
def writer():
"""A coroutine that writes data *sent* to it to fd, socket, etc."""
while True:
w = (yield)
print('>> ', w)
Теперь вопрос заключается в том, как функция-оболочка должна обрабатывать отправку данных в модуль записитак что любые данные, отправляемые в оболочку, прозрачно отправляются в 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
Оболочка должна принимать данные, которые отправляютсяк нему (очевидно) и должен также обрабатывать StopIteration
, когда цикл for исчерпан.Очевидно, что просто делать for x in coro: yield x
не будет.Вот версия, которая работает.
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
Или мы могли бы сделать это.
def writer_wrapper(coro):
yield from coro
Это экономит 6 строк кода, делает его намного более читабельным и просто работает.Магия!
Отправка данных в генератор приводит к - Часть 2 - Обработка исключений
Давайте усложним процесс.Что если нашему автору необходимо обработать исключения?Допустим, writer
обрабатывает SpamException
и печатает ***
, если встретит его.
class SpamException(Exception):
pass
def writer():
while True:
try:
w = (yield)
except SpamException:
print('***')
else:
print('>> ', w)
Что если мы не изменим writer_wrapper
?Это работает?Давайте попробуем
# 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
Хм, это не работает, потому что x = (yield)
просто вызывает исключение, и все останавливается.Давайте сделаем это, но вручную обработаем исключения и отправим их или выбросим в суб-генератор (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
Это работает.
# Result
>> 0
>> 1
>> 2
***
>> 4
Но так же и это!
def writer_wrapper(coro):
yield from coro
yield from
прозрачно обрабатывает отправку значений или сброс значений в суб-генератор.
Это все еще не охватывает все угловые случаи.Что произойдет, если внешний генератор закрыт?Как насчет случая, когда суб-генератор возвращает значение (да, в Python 3.3+ генераторы могут возвращать значения), как должно передаваться возвращаемое значение? То, что yield from
прозрачно обрабатывает все угловые корпуса, действительно впечатляет .yield from
просто волшебным образом работает и обрабатывает все эти случаи.
Я лично считаю yield from
плохим выбором ключевого слова, потому что это не делает природу двусторонней очевидной.Были предложены и другие ключевые слова (например, delegate
, но они были отклонены, поскольку добавить новое ключевое слово в язык гораздо сложнее, чем объединить существующие.
В целом, лучше думать о yield from
как о transparent two way channel
между вызывающим абонентом и вспомогательным генератором.
Ссылки:
- PEP 380 - Синтаксис для делегирования подчиненномугенератор (Ewing) [v3.3, 2009-02-13]
- PEP 342 - сопрограммы через расширенные генераторы (GvR, Eby) [v2.5, 2005-05-10]