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

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

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

[ update ]

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

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

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

6 ответов


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

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

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

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

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

кстати, если вы не уверены, что отправка данных в генератор даже означает, что вам нужно бросить все и прочитать о coroutines во-первых-они очень полезны (сравните их с подпрограммы), но к сожалению менее известный в 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

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

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

на yield from прозрачно обрабатывает отправку значений или бросание значений в подгенератор.

это все еще не охватывает все угловые случаи, хотя. Что происходит, если внешний генератор закрыт? Как насчет случая, когда подгенератор возвращает значение (да, в Python 3.3+ генераторы могут возвращать values), как должно распространяться возвращаемое значение? это yield from прозрачно обрабатывает все угловые случаи действительно впечатляет. yield from просто волшебно работает и обрабатывает все эти случаи.

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

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

ссылки:

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

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

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

for x in subgenerator:
  yield x

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

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

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

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

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


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

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 на функция coroutine. Оба из которых используются для приостановки выполнения coroutine.

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

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