Давайте сначала уберем одну вещь. Объяснение, 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]