Генераторы
Генератор это функция которая создает наборы значений. Обычная функция возвращает одно значение и после этого ее состояние уничтожается. Когда мы вызовем ее снова, выполнение начинается сначала и продолжается до возврата значение. Другими словами функция обеспечивает однократное выполнение. Но с генераторами Вы можете создать значение (ключевое слово yield) и поставить выполнение генератора на паузу. При этом поток выполнения передается вызывающему коду. Позже Вы можете возобновить выполнение генератора с того места на котором оно остановилось и получить новое значение (если конечно оно доступно). Рассмотрим пример:
1 2 3 4 5 6 7 8 |
def simple_gen(): yield "Hello" yield "World" gen = simple_gen() print(next(gen)) print(next(gen)) |
Важно отметить, что функция-генератор напрямую не возвращает ни одного значения, вместо этого она возвращает объект генератора, которые используется для итерации по значениям. Для того, что бы получить новое значение используется функция next(), при вызове которой из генератора выбираются значения, по одному за раз (это протокол итераций и кроме метода можно использовать цикл for).
Так что же полезного есть в генераторах. Предположим, Вам необходимо создать последовательность из чисел до 100 (такая себе упрощенная версия range). При этом пускай Вы сохранили значение в список. Но вдруг условия изменились и Вам нужен список из миллиардов значений, если попробовать сохранить их в список, то выйдет ошибка переполнения памяти. И это именно тот случай когда на сцену выходит генератор. Так как его главная особенность это создание значение на лету, без их сохранения, а также возможность создания бесконечных последовательностей. Например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def generate_nums(): num = 0 while True: yield num num = num + 1 nums = generate_nums() for x in nums: print(x) if x > 9: break |
В данном примере мы видим только первые 9-ть значений, но если убрать условие ограничивающие вывод, то вывод будет идти и идти, по одному значению за раз.
Заключение: функция генератора – это функция, которая может приостанавливать выполнение и генерировать несколько значений, а не просто возвращать одно значение. При вызове он дает нам объект-генератор, который действует как объект с итеративным интерфейсом. Мы можем использовать (итерировать) итерацию, чтобы получить значения одно за другим.
Корутина
Генератор создает значение и при этом он использует текущий контекст выполнения и конечно же позволяет поставить себя на паузу. Но если для решения задачи, требуется передать генератору некоторые данные? Для этих целей используются корутины. Ключевое слово yield может стоять справа от оператора равенства “=” внутри функции. При этом имеется возможность передать данные через метод send генератора. Например:
1 2 3 4 5 6 7 8 |
def coro(): hello = yield "Hello" yield hello c = coro() print(next(c)) print(c.send("World")) |
Данный код начинается с создания гнератора и получения от него значения привычным способом через next. Внутри генератора срабатывает часть блока yield ‘Hello’ и мы получаем результат “Hello”. При использовании метода send выполнение генератора возобновляется и значение параметра метода становится результатом yield, который присваивается в переменную. Когда используются корутины основанные на генераторах, то фактически корутина и генератор становится синонимами. Хотя они не совсем одно и то же, в таких случаях они очень часто используются взаимозаменяемо. Однако в Python 3.5 у нас есть ключевые слова async / await и встроенные корутины.
Python: Generators, Coroutines, Native Coroutines and async/await
Асинхронный ввод-вывод и модуль asyncio
Начиная с Python 3.4 существует новый модель asyncio, который вводит API для обобщенного асинхронного программирования. Мы можем использовать корутины с этим модулем для простого и понятного выполнения асинхронного кода. Мы можем использовать корутины вместе с модулем asyncio для простого выполнения асинхронных операций. Пример из официальной документации:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import asyncio import datetime import random @asyncio.coroutine def display_date(num, loop): end_time = loop.time() + 50.0 while True: print("Loop: {} Time: {}".format(num, datetime.datetime.now())) if (loop.time() + 1.0) >= end_time: break yield from asyncio.sleep(random.randint(0, 5)) loop = asyncio.get_event_loop() asyncio.ensure_future(display_date(1, loop)) asyncio.ensure_future(display_date(2, loop)) loop.run_forever() |
Мы создали функцию display_date(num, loop) которая принимает два аргумента, первый номер, а второй цикл событий, после чего наша корутина печатает текущее время. После чего используется ключевое слов yield from для ожидания результата выполнения asyncio.sleep – которая является корутиной, которая выполняется через указанное количество секунд (пауза выполнения), мы в своем коде передаем в эту функцию случайное количество секунд. После чего мы используем asyncio.ensure_future для планирования выполнения корутины в цикле событий. После чего мы указываем, что цикл событий должен работать бесконечно долго.
Если мы посмотрим на вывод программы, то увидим, что две функции выполняются одновременно. Когда мы используем yield from, цикл обработки событий знает, что он будет какое-то время занят, поэтому он приостанавливает выполнение функции и запускает другую. Таким образом, две функции работают одновременно (но не параллельно, поскольку цикл обработки событий является однопоточным).
Стоит отметить, yield from – это синтаксический сахар для for x in asyncio.sleep(random.randint(0, 5)): yield x – который делает код чище и проще.
Встроенные корутины
Помните, мы все еще используем функции на основе генератора? В Python 3.5 мы получили новые встроенные корутины, которые используют синтаксис async / await. Предыдущая функция может быть написана так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import asyncio import datetime import random async def display_date(num, loop, ): end_time = loop.time() + 50.0 while True: print("Loop: {} Time: {}".format(num, datetime.datetime.now())) if (loop.time() + 1.0) >= end_time: break await asyncio.sleep(random.randint(0, 5)) loop = asyncio.get_event_loop() asyncio.ensure_future(display_date(1, loop)) asyncio.ensure_future(display_date(2, loop)) loop.run_forever() |
Фактически изменены были только строки 6 и 12, для определения встроенной корутины определение функции помечается ключевым словом async, а вместо yield from используется await.
Корутины на генераторах и встроенные корутины
Функционально нет никакой разницы между корутинами на генераторах и встроенными корутинами, кроме различия в синтаксисе. Кроме того не допускается смешивания их синтаксисов. То есть нельзя использовать await внутри корутин на генераторах или yield / yeild from внутри встроенных корутин.
Несмотря на различия, мы можем организовывать взаимодействия между ними. Нам просто нужно добавить декоратор @types.coroutine к старым генераторам. Тогда мы можем использовать старый генератор из встроенных корутин и наоброт. Пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import asyncio import datetime import random import types @types.coroutine def my_sleep_func(): yield from asyncio.sleep(random.randint(0, 5)) async def display_date(num, loop, ): end_time = loop.time() + 50.0 while True: print("Loop: {} Time: {}".format(num, datetime.datetime.now())) if (loop.time() + 1.0) >= end_time: break await my_sleep_func() loop = asyncio.get_event_loop() asyncio.ensure_future(display_date(1, loop)) asyncio.ensure_future(display_date(2, loop)) loop.run_forever() |
Python: Generators, Coroutines, Native Coroutines and async/await