Давайте сначала уберем одну вещь. Объяснение, 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 для быстрого ознакомления.
Чтение данных из генератора с использованием yield из
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
это.
def reader_wrapper(g):
yield from g
Это работает, и мы исключили одну строку кода. И, вероятно, намерение немного яснее (или нет). Но жизнь ничего не меняет.
Отправка данных в генератор (сопрограмму) с использованием выхода из - Часть 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
дескрипторы a, 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]