в чем разница между yield from и yield в python 3.3.2+
после python 3.3.2 + python поддерживает новый синтаксис для создания функции генератора
yield from <expression>
Я сделал быструю попытку для этого
>>> def g():
... yield from [1,2,3,4]
...
>>> for i in g():
... print(i)
...
1
2
3
4
>>>
Это кажется простым в использовании, но PEP документ сложный. Мой вопрос в том, что есть ли какая-либо другая разница по сравнению с предыдущим утверждением yield? Спасибо.
3 ответов
для большинства применений, yield from
просто дает все из левой итерации в порядке:
def iterable1():
yield 1
yield 2
def iterable2():
yield from iterable1()
yield 3
assert list(iterable2) == [1, 2, 3]
для 90% пользователей, которые видят этот пост, я предполагаю, что это будет достаточным объяснением для них. yield from
просто представители к iterable на правой стороне.
Coroutines
однако, есть некоторые более эзотерические обстоятельства генератора, которые также имеют значение здесь. Менее известный факт о генераторах это то, что они могут использоваться как совместные процедуры. Это не очень распространено, но вы можете отправить данные в генератор, если хотите:
def coroutine():
x = yield None
yield 'You sent: %s' % x
c = coroutine()
next(c)
print(c.send('Hello world'))
в сторону: вам может быть интересно, какой вариант использования для этого (и вы не одиноки). Одним из примеров является contextlib.contextmanager
декоратор. Для распараллеливания определенных задач можно также использовать совместные процедуры. Я не знаю слишком много мест, где это используется, но Google app-engine ndb
datastore API использует его для асинхронных операций в довольно отличный способ.
теперь, предположим, что вы send
данные к генератору который производит данные от другого генератора ... Как оригинальный генератор получает уведомление? Ответ заключается в том, что это не так в python2.x где вам нужно обернуть генератор самостоятельно:
def python2_generator_wapper():
for item in some_wrapped_generator():
yield item
по крайней мере, не без много боли:
def python2_coroutine_wrapper():
"""This doesn't work. Somebody smarter than me needs to fix it. . .
Pain. Misery. Death lurks here :-("""
# See https://www.python.org/dev/peps/pep-0380/#formal-semantics for actual working implementation :-)
g = some_wrapped_generator()
for item in g:
try:
val = yield item
except Exception as forward_exception: # What exceptions should I not catch again?
g.throw(forward_exception)
else:
if val is not None:
g.send(val) # Oops, we just consumed another cycle of g ... How do we handle that properly ...
это все становится тривиальным с yield from
:
def coroutine_wrapper():
yield from coroutine()
, потому что yield from
действительно делегатов (все!) к основному генератору.
вернуться семантика
обратите внимание, что рассматриваемая ОПТОСОЗ также изменяет семантику возврата. Хотя это не прямо в вопросе OP, стоит быстро отступить, если вы готовы к этому. В Вместо python2.X, вы не можете сделать следующее:
def iterable():
yield 'foo'
return 'done'
это SyntaxError
. С обновлением до yield
, вышеуказанная функция не является законным. Опять же, основное применение-дело с сопрограммы (см. выше). Вы можете отправлять данные в генератор, и он может работать волшебным образом (возможно, используя потоки?) в то время как остальная часть программы делает другие вещи. Когда управление потоком возвращается к генератору,StopIteration
будет поднят (как обычно для конца генератора), но теперь StopIteration
будет иметь полезную нагрузку данных. Это то же самое, как если бы программист вместо этого написал:
raise StopIteration('done')
теперь вызывающий абонент может поймать это исключение и сделать что-то с полезной нагрузкой данных в интересах остальной части человечество.
на первый взгляд yield from
- алгоритмическое ярлык:
def generator1():
for item in generator2():
yield item
# do more things in this generator
что тогда в основном эквивалентно just:
def generator1():
yield from generator2()
# more things on this generator
на английском языке: при использовании внутри iterable, yield from
выдает каждый элемент в другой итерации, как если бы этот элемент исходил от первого генератора, с точки зрения кода, вызывающего первый генератор.
основным аргументом для его создания является возможность легкого рефакторинга кода, в значительной степени полагающегося на итераторы - код, который использует обычные функции, всегда может за очень небольшую дополнительную плату иметь блоки одной функции, рефакторированные к другим функциям, которые затем называются - что разделяет задачи, упрощает чтение и обслуживание кода и позволяет более повторно использовать небольшие фрагменты кода -
Итак, большие функции, как это:
def func1():
# some calculation
for i in somesequence:
# complex calculation using i
# ...
# ...
# ...
# some more code to wrap up results
# finalizing
# ...
может стать таким кодом, без недостатков:
def func2(i):
# complex calculation using i
# ...
# ...
# ...
return calculated_value
def func1():
# some calculation
for i in somesequence:
func2(i)
# some more code to wrap up results
# finalizing
# ...
однако при переходе к итераторам форма
def generator1():
for item in generator2():
yield item
# do more things in this generator
for item in generator1():
# do things
требует, чтобы для каждого элемента, потребляемый от generator2
, запущенный контекст сначала переключается на generator1
ничего не делается в этом контексте, и cotnext должен быть переключен на generator2
- и когда это дает значение, есть еще один промежуточный контекстный переключатель на generator1, прежде чем получить значение в фактический код, потребляющий эти значения.
С выходом от этих промежуточных переключателей контекста избегаются, которые могут сохранить довольно некоторые ресурсы, если есть много итераторов, связанных цепью: контекст переключается прямо из контекста, потребляющего внешний генератор, в самый внутренний генератор, пропуская контекст промежуточных генераторов вообще, пока внутренние не будут исчерпаны.
позже язык воспользовался этой "настройкой" через промежуточные контексты, чтобы использовать эти генераторы как подпрограммы: функции, которые могут выполнять асинхронные вызовы. С надлежащей базы, как описан в https://www.python.org/dev/peps/pep-3156/, эти подпрограммы написаны таким образом, что, когда они вызовут функцию, которая займет много времени для решения (из - за сетевой операции или интенсивной операции процессора, которая может быть выгружена в другой поток), этот вызов выполняется с помощью yield from
оператор-основной цикл фреймворка затем упорядочивает так, чтобы вызываемая дорогостоящая функция была правильно запланирована, и повторно выполняет (фреймворк mainloop всегда является кодом вызов самих подпрограмм). Когда дорогостоящий результат готов, фреймворк заставляет вызываемую ко-подпрограмму вести себя как исчерпанный генератор, и выполнение первой ко-подпрограммы возобновляется.
С точки зрения программиста, это как если бы код работал прямо вперед, без перерывов. С точки зрения процесса, совместная процедура была приостановлена в точке дорогостоящего вызова, а другие (возможно, параллельные вызовы той же совместной процедуры) продолжались бегущий.
Итак, можно написать как часть веб-искателя некоторый код:
@asyncio.coroutine
def crawler(url):
page_content = yield from async_http_fetch(url)
urls = parse(page_content)
...
который может одновременно извлекать десятки html-страниц при вызове из цикла asyncio.
Python 3.4 добавил asyncio
модуль для stdlib в качестве поставщика по умолчанию для такого рода функций. Он работал так хорошо, что в Python 3.5 было добавлено несколько новых ключевых слов, чтобы отличить совместные процедуры и асинхронные вызовы от использования генератора, описанный выше. Они описаны в https://www.python.org/dev/peps/pep-0492/
вот пример, который иллюстрирует это:
>>> def g():
... yield from range(5)
...
>>> list(g())
[0, 1, 2, 3, 4]
>>> def g():
... yield range(5)
...
>>> list(g())
[range(0, 5)]
>>>
yield from
дает каждый элемент iterable, но yield
дает сам iterable.