в чем разница между 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.