На практике, каковы основные направления использования нового синтаксиса "yield from" в Python 3.3?
Мне тяжело оборачивать свой мозг вокруг PEP 380.
- В каких ситуациях полезно использовать "yield from"?
- Какой классический вариант использования?
- Почему это по сравнению с микропотоками?
[ Обновить ]
Теперь я понимаю причину моих трудностей. Я использовал генераторы, но никогда не использовал сопрограммы (представленный PEP-342). Несмотря на некоторое сходство, генераторы и сопрограммы - это две разные концепции. Понимание сопрограмм (не только генераторов) является ключом к пониманию нового синтаксиса.
ИМХО сопрограммы - самая неясная особенность Python, большинство книг делают ее бесполезной и неинтересной.
Спасибо за отличные ответы, но особую благодарность agf и его комментариям, связанным с презентациями Дэвида Бизли. Дэвид качается.
11 ответов
Давайте сначала уберем одну вещь с пути. Объяснение того, что 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
обрабатывает 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
между абонентом и вспомогательным генератором.
Рекомендации:
В каких ситуациях полезно использовать "yield from"?
Каждая ситуация, когда у вас есть такой цикл:
for x in subgenerator:
yield x
Как описывает PEP, это довольно наивная попытка использования субгенератора, в нем отсутствуют некоторые аспекты, особенно правильная обработка .throw()
/.send()
/.close()
механизмы, представленные ПКП 342. Чтобы сделать это правильно, необходим довольно сложный код.
Какой классический вариант использования?
Учтите, что вы хотите извлечь информацию из рекурсивной структуры данных. Допустим, мы хотим получить все листовые узлы в дереве:
def traverse_tree(node):
if not node.children:
yield node
for child in node.children:
yield from traverse_tree(child)
Еще более важным является тот факт, что до yield from
Простого метода рефакторинга кода генератора не было. Предположим, у вас есть (бессмысленный) генератор, подобный этому:
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)
Теперь вы решили разделить эти циклы на отдельные генераторы. Без yield from
Это ужасно, вплоть до того момента, когда вы дважды подумаете, действительно ли вы хотите это сделать. С yield from
на самом деле приятно смотреть на:
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)
Почему это по сравнению с микропотоками?
Я думаю, что этот раздел в PEP говорит о том, что каждый генератор имеет свой собственный изолированный контекст выполнения. Вместе с тем, что выполнение переключается между генератором-итератором и вызывающим yield
а также __next__()
соответственно это похоже на потоки, где операционная система время от времени переключает исполняющий поток вместе с контекстом выполнения (стек, регистры, ...).
Эффект от этого также сопоставим: и генератор-итератор, и вызывающая программа одновременно переходят в состояние выполнения, их выполнения чередуются. Например, если генератор выполняет какие-то вычисления, а вызывающий абонент распечатывает результаты, вы увидите результаты, как только они станут доступны. Это форма параллелизма.
Эта аналогия не является чем-то конкретным для yield from
Впрочем, это довольно общее свойство генераторов в Python.
Короткий пример поможет вам понять один из yield from
вариант использования: получить значение из другого генератора
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]]])))
Везде, где вы вызываете генератор из генератора, вам нужен "насос", чтобыyield
ценности: for v in inner_generator: yield v
, Как указывает PEP, в этом есть тонкие сложности, которые большинство людей игнорируют. Нелокальное управление потоком throw()
один пример, приведенный в PEP. Новый синтаксис yield from inner_generator
используется везде, где вы бы написали явное for
цикл до. Это не просто синтаксический сахар: он обрабатывает все угловые случаи, которые игнорируются for
петля. Быть "сладким" поощряет людей использовать его и, таким образом, получать правильное поведение.
Это сообщение в ветке обсуждения говорит об этих сложностях:
С дополнительными функциями генератора, представленными в PEP 342, это уже не так: как описано в PEP Грега, простая итерация не поддерживает правильно send() и throw(). Гимнастика, необходимая для поддержки send() и throw(), на самом деле не так сложна, когда вы разбиваете их, но они также не тривиальны.
Я не могу говорить о сравнении с микропотоками, кроме как наблюдать, что генераторы - это тип паралеллизма. Вы можете считать приостановленный генератор потоком, который отправляет значения через yield
на потребительский поток. Реальная реализация может не иметь ничего общего с этим (и фактическая реализация, очевидно, представляет большой интерес для разработчиков Python), но это не касается пользователей.
Новый yield from
Синтаксис не добавляет никаких дополнительных возможностей для языка с точки зрения потоков, он просто облегчает правильное использование существующих функций. Точнее, начинающему потребителю сложного внутреннего генератора, написанного экспертом, легче проходить через этот генератор, не нарушая ни одной из его сложных функций.
yield
даст одно значение в коллекцию.
yield from
превратит коллекцию в коллекцию и сделает ее сглаженной.
Посмотрите этот пример:
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)
В консоли вы увидите:
A
B
C
A
B
C
A
B
C
yield from
в основном цепочки итераторов эффективным способом:
# 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
Как вы можете видеть, он удаляет один чистый цикл Python. Это почти все, что он делает, но связывание итераторов - довольно распространенный шаблон в Python.
Потоки - это в основном функция, которая позволяет вам выпрыгивать из функций в совершенно случайных точках и возвращаться в состояние другой функции. Супервизор потоков делает это очень часто, поэтому кажется, что программа запускает все эти функции одновременно. Проблема заключается в том, что точки случайны, поэтому вам нужно использовать блокировку, чтобы запретить супервизору остановить функцию в проблемной точке.
Генераторы очень похожи на потоки в этом смысле: они позволяют вам указывать конкретные точки (всякий раз, когда они yield
), где вы можете прыгать и выходить. При использовании этого способа генераторы называются сопрограммами.
Прочитайте этот превосходный учебник о сопрограмм в Python для более подробной информации
В прикладном использовании для сопрограммы асинхронного ввода-вывода, yield from
имеет такое же поведение, как await
в функции сопрограммы. Оба из которых используются, чтобы приостановить выполнение сопрограммы.
yield from
используется генератором сопрограммы.await
используется дляasync def
сопрограммная. (начиная с Python 3.5+)
Для Asyncio, если нет необходимости поддерживать более старую версию Python (например,> 3.5), async def
/ await
является рекомендуемым синтаксисом для определения сопрограммы. таким образом yield from
больше не нужен в сопрограмме.
Но в целом за пределами Asyncio, yield from <sub-generator>
все еще используется в итерации суб-генератора, как упоминалось в предыдущем ответе.
Этот код определяет функцию fixed_sum_digits
возвращает генератор, перечисляющий все шестизначные числа, так что сумма цифр равна 20.
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)
Попробуй написать без yield from
. Если вы найдете эффективный способ сделать это, дайте мне знать.
Я думаю, что в таких случаях: посещение деревьев, yield from
делает код проще и чище.
Проще говоря, yield from
обеспечивает хвостовую рекурсию для функций итератора.
дает от генератора до тех пор, пока генератор не станет пустым, а затем продолжит выполнение следующих строк кода .
например
def gen(sequence):
for i in sequence:
yield i
def merge_batch(sub_seq):
yield {"data": sub_seq}
def modified_gen(g, batch_size):
stream = []
for i in g:
stream.append(i)
stream_len = len(stream)
if stream_len == batch_size:
yield from merge_batch(stream)
print("batch ends")
stream = []
stream_len = 0
выполнение этого дает вам:
In [17]: g = gen([1,2,3,4,5,6,7,8,9,10])
In [18]: mg = modified_gen(g, 2)
In [19]: next(mg)
Out[19]: {'data': [1, 2]}
In [20]: next(mg)
batch ends
Out[20]: {'data': [3, 4]}
In [21]: next(mg)
batch ends
Out[21]: {'data': [5, 6]}
In [22]: next(mg)
batch ends
Out[22]: {'data': [7, 8]}
In [23]: next(mg)
batch ends
Out[23]: {'data': [9, 10]}
In [24]: next(mg)
batch ends
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
Input In [24], in <cell line: 1>()
----> 1 next(mg)
StopIteration:
Таким образом, yield from может получать выходные данные от другого генератора , вносить некоторые изменения , а затем передавать свой собственный вывод другим как сам генератор .
По моему скромному мнению, это один из основных вариантов использованияyield from
Я думаю, первые строки PEP380/ соответствующих новостей вполне это объясняют:
PEP 380 добавляет выход из выражения, позволяя генератору делегировать часть своих операций другому генератору. Это позволяет выделить часть кода, содержащую выход, и поместить ее в другой генератор.
Безyield from
, довольно сложно выделить части ваших сопрограмм.