На практике, каковы основные направления использования нового синтаксиса «yield from» в Python 3.3? - PullRequest
315 голосов
/ 14 марта 2012

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

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

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

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

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

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

Ответы [ 6 ]

455 голосов
/ 30 сентября 2014

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

Чтение данных из генератора, используя выход из

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 it.

def reader_wrapper(g):
    yield from g

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

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

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

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]
78 голосов
/ 14 марта 2012

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

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

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

28 голосов
/ 14 марта 2012

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

19 голосов
/ 27 декабря 2016

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

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

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

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

3 голосов
/ 15 марта 2012

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...