Home

apirobot

02 Feb 2018

Генераторы и корутины в Python

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

Генераторы

Генератор - функция, которая генерирует последовательность значений, вместо одного значения, как это делает обычная функция. Любая функция, в которой есть оператор yield является генераторной:

>>> def fibonacci():
...     a, b = 0, 1
...     while True:
...         yield a
...         a, b = b, a + b

>>> # Работаем с функцией вручную
>>> f = fibonacci()
<generator object fibonacci at 0x7f1d96f56990>
>>> next(f)
0
>>> next(f)
1
>>> ...

>>> # Через цикл for
>>> for num in fibonacci():
...     if num > 42:
...         break  # иначе цикл будет бесконечный
...     print(num)
0
1
1
2
3
...

Функция порождает (производит) числа Фибоначчи по одному через оператор yield. После каждого yield, генераторная функция приостанавливается и выполнение программы переходит к вызывающей стороне. Генераторная функция продолжает работу после вызова функции next(…).

В примере с числами Фибоначчи, генераторная функция работает бесконечно. Она ничего не возвращает (return). Если генераторная функция завершает работу и в конце возвращает какое-то значение, то после этого выбрасывается исключение StopIteration. Это исключение можно словить и получить значение, которое вернул генератор:

>>> def gen():
...     yield 'Yield something'
...     return 'Return something'

>>> g = gen()
>>> next(g)
'Yield something'
>>> try:
...     next(g)
... except StopIteration as exc:
...     print(exc.value)
Return something

Напомню, что любая функция возвращает какое-то значение. Если оператор return не указан явно, то функция возвращает None. Поэтому, после завершения работы генераторной функции, исключение StopIteration выбрасывается в любом случае.

Корутины

Генераторная функция порождает значения для вызывающей стороны через оператор yield. Однако, генераторная функция также может и получать значения от вызывающей стороны. Генераторы, которые получают данные от вызывающей стороны называются корутинами (сопрограммами на русском). Все корутины являются генераторами. Но не наоборот, не путайте.

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

>>> def double():
...     print('> Начало функции')
...     value = 2 * (yield)
...     print('> value = {}'.format(value))
...     yield value
...     print('> Конец функции')

>>> d = double()
>>> next(d)
> Начало функции
>>> d.send(21)  # метод send объекта генератора передает данные в функцию
> value = 42
42
>>> d.send(42)
> Конец функции
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Последовательность работы функции double показана на картинке:

Double

Как вы уже поняли, yield - двухсторонний оператор. Сначала генераторная функция передает значение вызывающей стороне (yield something). Затем останавливается и ждет, пока вызывающая сторона не передаст ей что-нибудь в ответ (generator.send(something)), чтобы она могла сохранить это значение (something = yield) и продолжить работу до следующего оператора yield.

yield является двухсторонним оператором всегда, даже если вы не передаете значение генератору через send, а просто пытаетесь продолжить работу генератора через next:

>>> def hello():
...     value = yield 'Hello'
...     print('value = {}'.format(value))

>>> gen = hello()
>>> next(gen)
'Hello'
>>> next(gen)
value = None  # так как мы ничего не передали в генератор
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Аналогично и в ситуации, когда yield используется только для получения данных. В таком случае, yield передает None вызывающей стороне:

>>> def simple_coroutine():
...     value = yield
...     print(value)

>>> coro = simple_coroutine()
>>> from_coro = next(coro)
>>> print(from_coro)
None
>>> coro.send(42)
42
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Заключение

В статье разобрались с оператором yield, и в чем разница между генераторами и корутинами.

Хоть генераторы и чем-то схожи с корутинами, но корутины довольно объемная тема, в которой много чего еще интересного. От оператора yield from и до пакета asyncio, который, по сути, работает на корутинах. В статье я затронул лишь самые основы.

До следующего раза,
apirobot в 08:00

comments powered by Disqus