На практике, каковы основные применения нового синтаксиса «yield from» в Python 3.3?


407

Мне трудно оборачивать свой мозг вокруг PEP 380 .

  1. В каких ситуациях полезно использовать «yield from»?
  2. Какой классический вариант использования?
  3. Почему это по сравнению с микропотоками?

[ Обновить ]

Теперь я понимаю причину моих трудностей. Я использовал генераторы, но никогда не использовал сопрограммы (представленный PEP-342 ). Несмотря на некоторое сходство, генераторы и сопрограммы - это две разные концепции. Понимание сопрограмм (не только генераторов) является ключом к пониманию нового синтаксиса.

ИМХО сопрограммы - самая неясная особенность Python , большинство книг делают ее бесполезной и неинтересной.

Спасибо за отличные ответы, но особую благодарность agf и его комментариям, связанным с презентациями Дэвида Бизли . Дэвид качается.



7
Видеозапись презентации Дэвида Бизли на сайте dabeaz.com/coroutines : youtube.com/watch?v=Z_OAlIhXziw
jcugat,

Ответы:


572

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

Ссылки:

  1. PEP 380 - Синтаксис для делегирования субгенератору (Ewing) [v3.3, 2009-02-13]
  2. PEP 342 - сопрограммы через расширенные генераторы (GvR, Eby) [v2.5, 2005-05-10]

3
@PraveenGollakota, во второй части вашего вопроса, Отправка данных в генератор (сопрограмму) с использованием yield из - Часть 1 , что, если у вас есть больше, чем сопрограмм, чтобы переслать полученный элемент? Как в случае с вещателем или подписчиком, когда вы предоставляете несколько сопрограмм для оболочки в вашем примере, и элементы должны быть отправлены всем или подмножеству?
Кевин Габузи,

3
@PraveenGollakota, Слава за отличный ответ. Небольшие примеры позволяют мне попробовать вещи в Repl. Ссылка на курс Дейва Бизли была бонусом!
BiGYaN

1
выполнение except StopIteration: passINSIDE while True:цикла не является точным представлением yield from coro- которое не является бесконечным циклом и после того, как coroоно исчерпано (то есть вызывает StopIteration), writer_wrapperвыполнит следующий оператор. После последнего заявления он сам автоматически поднимется, StopIterationкак любой измотанный генератор ...
Aprillion

1
... так что если вместо этого writerсодержится , то после печати он ТАКЖЕ автоматически поднимет, и это будет автоматически обработано, а затем автоматически поднимет его собственный, и, поскольку он не находится внутри блока, он будет фактически поднят в этот момент ( то есть traceback будет сообщать только строку , а не что-либо изнутри генератора)for _ in range(4)while True>> 3StopIterationyield fromwriter_wrapperStopIterationwrap.send(i)trywrap.send(i)
Aprillion

3
После прочтения « даже не начинаешь судить » я знаю, что пришел к правильному ответу. Спасибо за отличное объяснение!
Hot.PxL

89

В каких ситуациях полезно использовать «yield from»?

В каждой ситуации, когда у вас есть такой цикл:

for x in subgenerator:
  yield x

Как описывает PEP, это довольно наивная попытка использования субгенератора, в нем отсутствуют некоторые аспекты, особенно правильная обработка механизмов .throw()/ .send()/ .close(), введенных в PEP 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.


Рефакторинг генераторов сегодня болезнен .
Джош Ли

1
Я часто использую itertools для рефакторинга генераторов (например, itertools.chain), это не так уж важно. Мне нравится доходность от, но я все еще не вижу, насколько это революционно. Вероятно, это так, поскольку Гвидо все без ума от этого, но я, должно быть, скучаю по общей картине. Я думаю, это здорово для send (), так как это трудно реорганизовать, но я использую это не часто.
Э-Удовлетворение

Я полагаю, что get_list_values_as_xxxэто простые генераторы с одной строкой for x in input_param: yield int(x)и двумя другими соответственно с strиfloat
madtyn

@NiklasB. re "извлекать информацию из рекурсивной структуры данных." Я просто вхожу в Py для данных. Не могли бы вы сделать удар в этот вопрос ?
alancalvitti

33

Где бы вы вызываете генератор изнутри генератора вам нужно «прокачать» повторно 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синтаксис не добавляет никаких дополнительных возможностей к языку с точки зрения потоков, он просто упрощает правильное использование существующих функций. Точнее, начинающему потребителю сложного внутреннего генератора, написанного экспертом, легче проходить через этот генератор, не нарушая ни одной из его сложных функций.


23

Короткий пример поможет вам понять один из вариантов 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]]])))

2
Просто хотел предположить, что печать в конце выглядела бы немного лучше без преобразования в списокprint(*flatten([1, [2], [3, [4]]]))
yoniLavi

6

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 для более подробной информации


10
Этот ответ вводит в заблуждение, потому что он исключает характерную особенность «yield from», как упоминалось выше: поддержка send () и throw ().
Джастин W

2
@Justin W: Полагаю, все, что вы читали раньше, на самом деле вводит в заблуждение, потому что вы не поняли, что throw()/send()/close()это за yieldфункции, которые, yield fromочевидно, должны реализовываться должным образом, поскольку это должно упростить код. Такие мелочи не имеют ничего общего с использованием.
Йохен Ритцель

5
Вы оспариваете ответ Бена Джексона выше? Мое прочтение вашего ответа таково: это по сути синтаксический сахар, который следует за преобразованием кода, которое вы предоставили. Ответ Бена Джексона специально опровергает это утверждение.
Джастин W

@JochenRitzel Вам никогда не нужно писать свою собственную chainфункцию, потому что она itertools.chainуже существует. Использование yield from itertools.chain(*iters).
Acumenus

5

В прикладном использовании для сопрограммы асинхронного ввода - вывода , yield fromимеет аналогичное поведение как awaitв сопрограммах функции . Оба из которых используются, чтобы приостановить выполнение сопрограммы.

Для Asyncio, если нет необходимости поддерживать более старую версию Python (т.е.> 3.5), async def/ awaitявляется рекомендуемым синтаксисом для определения сопрограммы. Таким образом yield from, больше не требуется сопрограмма.

Но в целом за пределами asyncio yield from <sub-generator>все еще используется итерация вспомогательного генератора, как упоминалось в предыдущем ответе.


1

Этот код определяет функцию, 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делает код проще и чище.


Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.