На практике, каковы основные способы использования нового синтаксиса "yield from" в Python 3.3?
Мне трудно обернуть свой мозг вокруг PEP 380.
- каковы ситуации, когда "выход из" полезен?
- что такое классический случай использования?
- почему он сравнивается с микро-потоками?
[ обновление ]
теперь я понимаю причину моих трудностей. Я использовал генераторы, но никогда не использовал сопрограммы (введенные PEP-342). Несмотря на некоторые сходства, генераторы и сопрограммы-это в основном две разные концепции. Понимание сопрограмм (а не только генераторов) является ключом к пониманию нового синтаксиса.
ИМХО сопрограммы-это самая непонятная функция Python большинство книг сделать его бесполезным и неинтересным.
Спасибо за отличные ответы, но особая благодарность agf и его комментарий, ссылающийся на презентации Дэвида Бизли. Дэвид Рокс.
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
между абонентом и суб-генератора.ссылки:
каковы ситуации, когда "выход из" полезен?
каждая ситуация, когда у вас есть цикл, как это:
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
) где вы можете прыгать и выходить. При использовании этого способа генераторы называются сопрограммами.
в прикладном использовании для асинхронный IO coroutine,
yield from
имеет такое же поведение, какawait
на функции сопрограмма. Оба из которых используются для приостановки выполнения сопрограммы.
yield from
используется генератор на основе сопрограмм.
await
используетсяasync def
coroutine. (начиная с Python 3.5+)для Asyncio, если нет необходимости поддерживать старую версию Python (т. е. >3.5),
async def
/await
рекомендуемый синтаксис для определения сопрограмм. Таким образомyield from
больше не требуется в сопрограмме.но в целом за пределами ввода-вывода,
yield from <sub-generator>
имеет еще некоторое другое использование в итерации суб-генератор как упоминалось в предыдущем ответе.