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

Мне сложно переносить мой мозг вокруг PEP 380.

  • Каковы ситуации, когда "доходность" полезна?
  • Что такое классический вариант использования?
  • Почему это сравнивается с микропотоками?

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

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

IMHO сопрограммы - самая неясная функция Python, большинство книг делают ее бесполезной и неинтересной.

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

Ответ 1

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

Подумайте о yield from как о предоставлении прозрачного двухстороннего канала от caller до sub-generator. Это включает как получение данных, так и отправку данных в sub-generator. Кстати, если вы не уверены, что отправка данных в генератор даже означает, вам нужно все бросить и сначала прочитать о сопрограммах. Дэйв Бэйзли Любопытный Курс на Курутинах - отличное начало. Прочитайте слайды 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

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

Отправка данных в генератор (сопрограмма) с использованием выхода из - Часть 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 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

Um, он не работает, потому что 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, генераторы могут возвращать значения), как должно распространяться возвращаемое значение? Все угловые случаи, которые yield from обрабатывает прозрачно, действительно впечатляют. yield from просто магически работает и обрабатывает все эти случаи.

Я лично чувствую, что yield from - это плохой выбор ключевого слова, потому что он не делает очевидным двусторонний характер. Были предложены другие ключевые слова (например, delegate, но были отклонены, потому что добавление нового ключевого слова на язык намного сложнее, чем объединение существующих).

В общем, лучше всего думать о yield from как transparent two way channel между caller и sub-generator.

Литература:

  • PEP 380 - Синтаксис для делегирования на подгенератор (Ewing) [v3.3, 2009-02-13]
  • PEP 342 - Coroutines через Enhanced Generators (GvR, Eby) [v2.5, 2005-05-10]

Ответ 2

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

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

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__(), соответственно, это похоже на потоки, в которых операционная система время от времени переключает исполняемый поток, а также выполнение контекст (стек, регистры,...).

Эффект этого также сопоставим: как генератор-итератор, так и прогресс вызывающего абонента в их состоянии выполнения одновременно, их выполнение чередуется. Например, если генератор выполняет какое-то вычисление, и вызывающий абонент распечатывает результаты, вы увидите результаты, как только они будут доступны. Это форма concurrency.

Эта аналогия не имеет ничего общего с yield from, хотя это скорее общее свойство генераторов в Python.

Ответ 3

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

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

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

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

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

Ответ 4

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

Ответ 5

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