http://habrahabr.ru/post/266743/
Тихо и незаметно (с),
вышел Python версии 3.5! И, безусловно, одно из самых интересных нововведений релиза является новый синтаксис определения сопрограмм с помощью ключевых слов
async/await, далее в статье об этом.
Поверхностный просмотр
«PEP 0492 — Coroutines with async and await syntax» по началу оставил у меня вопрос «Зачем это надо». Сопрограммы удовлетворительно реализуются на расширенных генераторах и на первый взгляд может показаться, что все свелось к замене
yield from на
await, а декоратора, создающего сопрограмму на
async. Сюда можно добавить и возникающее ощущение, что все это сделано исключительно для использования с модулем
asyncio.
Но это, конечно же, не так, тема глубже и интереснее.coroutine
Главное, наверное, это то, что теперь сопрограмма в Python — это специальный объект
native coroutine, а не каким-то специальным образом оформленный генератор или еще что-то. Этот объект имеет методы и функции стандартной библиотеки для работы с ним. То есть теперь, это объект, определяемый как часть языка.
await
К сожалению, не нашел в документации и PEP краткое определение для чего введено это новое ключевое слово. Рискну сформулировать его сам:
Ключевое слово await указывает, что при выполнении следующего за ним выражения возможно переключение с текущей сопрограммы на другую или на основной поток выполнения.
Соответственно выражение после
await тоже не простое, это должен быть
awaitable объект.
awaitable object
Есть три варианта awaitable объектов:
- Другая сопрограмма, а именно объект native coroutine. Этот напоминает, и видимо реализовано аналогично случаю, когда в генераторе с помощью yield from вызывается другой генератор.
- Сопрограмма на основе генератора, созданная с помощью декоратора types.coroutine(). Это вариант обеспечения совместимости с наработками, где сопрограммы реализованы на основе генераторов.
- Специальный объект, у которого реализован магический метод __await__, возвращающий итератор. С помощью этого итератора реализуется возврат результата выполнения сопрограммы.
Примера, как написать свой
awaitable объект ни в PEP, ни в документации не нашел, во всяком случае на момент написания статьи этого нет. Ниже этот недостаток будет исправлен.)
async
В РЕР десяток абзацев с заголовками «Why ...» и «Why not ...». Почти все они посвящены вопросу, почему это ключевое слово используется так, а не как-то иначе. И действительно,
async def смотрится в коде странно, и вызывает размышления на тему а «pythonic way» ли это? С другой стороны, понятно, что хотели какой-то более целостной картины, так как есть еще и
async for и
async with.
- async def — определяет native coroutine function, результатом вызова которой будет объект-сопрограмма native coroutine, пока еще не запущенная.
- async for — определяет, что итератор используемый в цикле, при получении следующего значения может переключать выполнение с текущей сопрограммы. Объект итератор имеет вместо стандартных магических методов: __iter__ и __next__, методы: __aiter__ и __anext__. Функционально они аналогичны, но как следует из определения, допускают использования await в своем теле.
- async with — определяет, что при входе в контекстный блок и выходе из него может быть переключение выполнения с текущей сопрограммы. Так же, как и в случае с асинхронным генератором, вместо магических методов: __enter__ и __exit__ следует использовать функционально аналогичные __aenter__ и __aexit__.
В определениях, которые даны в PEP написано, что в магических методах может вызываться «асинхронный код». Мне кажется, «переключение выполнения с текущей сопрограммы» более правильный вариант. Использование термина «асинхронный код» может ввести в заблуждение, потому-что «асинхронный код» часто реализуется на функциях обратного вызова, а это немного другая тема.
Примеры на использование асинхронных итераторов и контекст менеджеров в документации и PEP достаточно, usecase в общем-то понятен и все логично. Непонятно только одно — зачем использовать версии магических методов с другими именами, ведь они все равно объявляются с использованием `async def`. Видимо, это что-то, связанное с особенностями реализации, другого объяснения не вижу.
Как это готовить?
Изучение какой-то новой фичи языка или библиотеки быстро упирается в вопрос, как и где это использовать. И более глубокое изучение, на мой взгляд, стоит продолжать уже на практическом примере. Для меня, если тема связана с сопрограммами, асинхронностью и тому подобными вещами, такой практический пример — это написание хеллоуворда, использующего event-driven подход. Формулировка задачи такая: «Вызов функции sleep должен остановить исполнение сопрограммы на определенное время».
Сопрограммы и event-driven прекрасно сочетаются, в
другой моей статье более подробно, почему я так считаю. И пример такого рода хорошо нам продемонстрирует почти все возможности и нюансы использования сопрограмм.
Предположим, что мы имеем диспетчер событий, запущенный в основном потоке исполнения, который при возникновении ожидаемых событий вызывает функции обратного вызова. Тогда практическая реализация может быть такой:
- Функция sleep настраивает диспетчер событий на вызов функции обратного вызова через заданный промежуток времени. После этого переключает управление в основной поток исполнения (то есть на диспетчер).
- Переданная в диспетчер функция обратного вызова вызывается по истечении заданного времени. В ней переходит переключение на сопрограмму с передачей ей какой-то полезной информации.
Закодировано это может быть как-то так:
from time import time
from collections import deque
from tornado.ioloop import IOLoop
current = deque()
class sleep(object):
def __init__(self, timeout):
self.deadline = time() + timeout
def __await__(self):
def swith_to(coro):
current.append(coro)
coro.send(time())
IOLoop.instance().add_timeout(self.deadline, swith_to, current[0])
current.pop()
return (yield)
def coroutine_start(run, *args, **kwargs):
coro = run(*args, **kwargs)
current.append(coro)
coro.send(None)
if __name__ == '__main__':
async def hello(name, timeout):
while True:
now = await sleep(timeout)
print("Hello, {}!\tts: {}".format(name, now))
coroutine_start(hello, "Friends", 1.0)
coroutine_start(hello, "World", 2.5)
IOLoop.instance().start()
Как видите, код краткий, достаточно понятный и, кстати, рабочий. Опишу основные моменты в нем:
- В качестве диспетчера событий использован tornado.ioloop.IOLoop комментировать по моему тут особо нечего.
- Класс sleep — реализует awaitable объект, его функция — передать управление в диспетчер событий, предварительно настроив его на вызов callback через заданный промежуток времени.
- Функция обратного вызова определена как замыкание, но в данном случае это не играет никакой роли. Назначение ее — просто переключить выполнение назад на сопрограмму с передачей текущего времени. Переключение выполнения на сопрограмму, производится вызовом ее метода send или метода throw для переключения с выбросом исключения.
- Назначение функции coroutine_start — это создать сопрограмму, вызвав функцию фабрику и запустить ее на выполнение. Первый вызов метода send сопрограммы, обязательно должен быть с параметром None — это запускает сопрограмму
- Сама функция hello тривиальна. Может и так понятно, но думаю стоит уточнить. Эта функция не сопрограмма! Эта функция, которая создает и возвращает сопрограмму ( функция-фабрика), аналогично функциям, создающим и возвращающим генератор.
Развитие этой идеи: «async/await coroutine and event-driven», можно посмотреть
по этой ссылке. Оно еще сырое, но кроме продемонстрированного переключения по событию «timeout», реализовано переключение сопрограмм по событиям «I/O ready» и «system sygnal». В качестве демо, есть пример асинхронного echo server.
В заключение
Сопрограммы в том или ином виде были доступны уже достаточно давно, но не было «официальных правил игры с ними». Теперь эти «правила игры» определены и зафиксированы и это станет хорошим поводом более широко использовать в Python методы асинхронного программирования.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.