На практике, каковы основные способы использования нового синтаксиса "yield from" в Python 3.3?


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

  1. каковы ситуации, когда "выход из" полезен?
  2. что такое классический случай использования?
  3. почему он сравнивается с микро-потоками?

[ обновление ]

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

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

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

6 243

6 ответов:

давайте сначала разберемся с одной вещью. Объяснение, что yield from g эквивалентно for v in g: yield vдаже не начинает вершить правосудие к чему yield from - все. Потому что, давайте посмотрим правде в глаза, если все yield from это расширит for цикл, то это не гарантирует добавление yield from на язык и исключить целую кучу новых функций от реализации в Python 2.x.

что yield from это устанавливает прозрачный двунаправленная связь между абонентом и суб-генератор:

  • соединение является "прозрачным" в том смысле, что оно будет распространять все правильно, а не только генерируемые элементы (например, распространяются исключения).

  • подключение "двунаправленный" в том смысле, что данные могут быть отправлены с и до a генератор.

(если бы мы говорили о TCP,yield from g может означать "теперь временно отключите сокет моего клиента и снова подключите его к этому другому сокету сервера".)

кстати, если вы не уверены, что отправка данных в генератор даже означает, что вам нужно бросить все и прочитать о сопрограммы во-первых-они очень полезны (сравните их с подпрограммы), но к сожалению менее известный в Python. Дейв Бизли на Couroutines это отличное начало. читать слайды 24-33 краткий обзор.

чтение данных из генератора с использованием выхода из

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

это работает, и мы исключили одну строку кода. И, вероятно, намерение немного яснее (или нет). Но ничего жизнь изменение.

отправка данных в генератор (сопрограмма) с помощью yield from-Part 1

теперь давайте сделаем что-то более интересное. Давайте создадим сопрограмму под названием writer который принимает данные, отправленные ему и записывает в сокет, fd и т. д.

def writer():
    """A coroutine that writes data *sent* to it to fd, socket, etc."""
    while True:
        w = (yield)
        print('>> ', w)

теперь вопрос в том, как функция-оболочка должна обрабатывать отправку данных в writer, чтобы любые данные, отправленные в оболочку, были прозрачное отправлено 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

The 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]

каковы ситуации, когда "выход из" полезен?

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

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.

везде, где вы вызываете генератор изнутри генератора, вам нужен "насос" для повторногоyield значения: for v in inner_generator: yield v. Как указывает PEP, в этом есть тонкие сложности, которые большинство людей игнорируют. Нелокальное управление потоком, например throw() является одним из примеров, приведенных в ОПТОСОЗ. Новый синтаксис yield from inner_generator используется везде, где вы бы написали явное for петли перед. Однако это не просто синтаксический сахар: он обрабатывает все угловые случаи, которые игнорируются for петли. Быть "сладким" побуждает людей использовать его и, таким образом, получить правильное поведение.

данное сообщение в теме говорит об этих сложностях:

С дополнительными характеристиками генератора введенными PEP 342, то нет более длинный случай: как описано в PEP Грега, простая итерация не делает поддержка отправить() и бросить () правильно. Гимнастика нужна для поддержки отправить () и бросить () на самом деле не так сложно, когда вы ломаете их вниз, но они тоже не тривиальны.

Я не могу говорить с сравнение С микро-потоками, кроме как наблюдать, что генераторы являются одним из видов паралеллизма. Вы можете считать приостановленный генератор потоком, который отправляет значения через yield к потребительскому потоку. Фактическая реализация может быть ничем подобным (и фактическая реализация, очевидно, представляет большой интерес для разработчиков Python), но это не касается пользователи.

новая yield from синтаксис не добавляет никаких дополнительных возможностей к языку с точки зрения потоковой передачи, он просто упрощает правильное использование существующих функций. Или точнее это делает его легче для новичок потребитель сложного внутреннего генератора, написанного эксперт пройти через этот генератор, не нарушая ни одной из его сложных функций.

короткий пример поможет вам понять один из yield from ' s use case: получить значение из другого генератора

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

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

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

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