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

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

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

[ Обновить ]

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

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

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

11 ответов

Решение

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

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

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

for x in subgenerator:
  yield x

Как описывает PEP, это довольно наивная попытка использования субгенератора, в нем отсутствуют некоторые аспекты, особенно правильная обработка .throw()/.send()/.close() механизмы, представленные ПКП 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 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 ценности: 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 Синтаксис не добавляет никаких дополнительных возможностей для языка с точки зрения потоков, он просто облегчает правильное использование существующих функций. Точнее, начинающему потребителю сложного внутреннего генератора, написанного экспертом, легче проходить через этот генератор, не нарушая ни одной из его сложных функций.

yield даст одно значение в коллекцию.

yield from превратит коллекцию в коллекцию и сделает ее сглаженной.

Посмотрите этот пример:

      def yieldOnly():
    yield "A"
    yield "B"
    yield "C"

def yieldFrom():
    for i in [1, 2, 3]:
        yield from yieldOnly()

test = yieldFrom()
for i in test:
print(i)

В консоли вы увидите:

      A
B
C
A
B
C
A
B
C

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

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

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

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

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

Проще говоря, yield fromобеспечивает хвостовую рекурсию для функций итератора.

дает от генератора до тех пор, пока генератор не станет пустым, а затем продолжит выполнение следующих строк кода .

например

      def gen(sequence):
    for i in sequence:
        yield i


def merge_batch(sub_seq):
    yield {"data": sub_seq}

def modified_gen(g, batch_size):
    stream = []
    for i in g:
        stream.append(i)
        stream_len = len(stream)
        if stream_len == batch_size:
            yield from merge_batch(stream)
            print("batch ends")
            stream = []
            stream_len = 0

выполнение этого дает вам:

      In [17]: g = gen([1,2,3,4,5,6,7,8,9,10])
In [18]: mg = modified_gen(g, 2)
In [19]: next(mg)
Out[19]: {'data': [1, 2]}

In [20]: next(mg)
batch ends
Out[20]: {'data': [3, 4]}

In [21]: next(mg)
batch ends
Out[21]: {'data': [5, 6]}

In [22]: next(mg)
batch ends
Out[22]: {'data': [7, 8]}

In [23]: next(mg)
batch ends
Out[23]: {'data': [9, 10]}

In [24]: next(mg)
batch ends
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
Input In [24], in <cell line: 1>()
----> 1 next(mg)

StopIteration: 

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

По моему скромному мнению, это один из основных вариантов использованияyield from

Я думаю, первые строки PEP380/ соответствующих новостей вполне это объясняют:

PEP 380 добавляет выход из выражения, позволяя генератору делегировать часть своих операций другому генератору. Это позволяет выделить часть кода, содержащую выход, и поместить ее в другой генератор.

Безyield from, довольно сложно выделить части ваших сопрограмм.

Другие вопросы по тегам