Поддержка асинхронности async/await в Python
Внимание! Любой асинхронный код написанный на языке Python не будет работать без поддержки его распараллеливания во время выполнения.
Asyncio используется в качестве основы для нескольких асинхронных сред Python, которые предоставляют высокопроизводительные сетевые и веб-серверы, библиотеки подключений к базам данных, распределенные очереди задач и т. д.
Есть определенный список правил, касающийся использования команд async / await .
- Функция, которая начинается с async def является сопрограммой.
- В теле сопрограммы можно использовать выражения await , return или yield .
- Использование в сопрограмме ключевого слова yield создает асинхронный генератор, через который можно итерироваться с помощью async for .
- Использование в сопрограмме выражения async with запускает асинхронные контекстные менеджеры.
- Сопрограммы не могут использовать выражение yield from . Это вызовет исключение SyntaxError .
- Ключевое слово await можно использовать только в теле сопрограммы. Вызов await в другом месте спровоцирует исключение SyntaxError .
- Ключевое слово await требует наличия awaitable объекта. Такими объектами могут быть другая сопрограмма или объект у которого определен метод __await__() .
Ключевое слово await позволяет сопрограмме отдать контроль назад в главный цикл, который содержит порядок исполнения всех сопрограмм.
async def process(): result = await func() return result
Если Python встречает ключевое слово await то это можно описать так: — «отложить исполнение кода сопрограммы process() до тех пор, пока я не получу результат выполнения func() . В это время я займусь чем-нибудь другим».
Сопрограммы/корутины async def в Python
Внутри тела функции сопрограммы идентификаторы await и async становятся зарезервированными ключевыми словами. Выражения await, async for и async with могут использоваться только в телах функций сопрограмм.
Асинхронный async for в Python
Асинхронный оператор `async for . in` обеспечивает удобную итерацию по асинхронным итераторам и асинхронным генераторам. Асинхронный оператор `async for . in` действует только в теле асинхронной функции (сопрограммы) `async def`.
Асинхронный контекст-менеджер async with в Python
Асинхронный менеджер контекста может приостановить выполнение в своих методах `__aenter__()` и `__aexit__()`. Асинхронный оператор `with` может использоваться только в теле асинхронной функции (сопрограммы).
Асинхронный итератор в Python
Асинхронный итератор может вызывать асинхронный код в своем методе `__anext__` и могут использоваться только в асинхронном операторе `async for`.
Асинхронный генератор в Python
Наличие выражения yield в функции или методе, определенном с использованием async def, дополнительно определяет функцию как функцию асинхронного генератора.
- ОБЗОРНАЯ СТРАНИЦА РАЗДЕЛА
- Сопрограммы/корутины async def
- Асинхронный async for
- Асинхронный контекст-менеджер async with
- Асинхронный итератор
- Асинхронный генератор
Сопрограммы/корутины async def в Python
Асинхронные функции (сопрограммы) являются более обобщенной формой подпрограмм.
Подпрограммы вводятся в одной точке и выходят в другой точке. Выполнение сопрограмм в Python может быть приостановлено и возобновлено в разных точках. Они могут быть реализованы с помощью оператора async def .
async def coroutine(param1, param2): do_stuff() await some_coroutine()
Внутри тела функции сопрограммы идентификаторы await и async становятся зарезервированными ключевыми словами. Выражения await , async for и async with могут использоваться только в телах функций сопрограмм.
Примеры:
import asyncio async def ticker(delay, to): for i in range(to): yield (i, delay) await asyncio.sleep(delay) async def run(k): async for i in ticker(k, 5): print(i) async def main(): task1 = asyncio.create_task(run(0.5)) task2 = asyncio.create_task(run(1)) # планируем одновременные вызовы: await asyncio.gather(task1, task2) if __name__ == '__main__': asyncio.run(main()) # (0, 0.5) # (0, 1) # (1, 0.5) # (1, 1) # (2, 0.5) # (3, 0.5) # (2, 1) # (4, 0.5) # (3, 1) # (4, 1)
Понимание сопрограмм.
Сопрограмма — это такая сущность, которой можно передавать и получать назад управление, передавать и получать данные и которая к тому же хранит внутреннее состояние. Сопрограммы являются естественным способом выражения многих алгоритмов, таких как симуляции, игры, асинхронный ввод-вывод и другие формы программирования, управляемого событиями, или совместной многозадачности.
- ОБЗОРНАЯ СТРАНИЦА РАЗДЕЛА
- Сопрограммы/корутины async def
- Асинхронный async for
- Асинхронный контекст-менеджер async with
- Асинхронный итератор
- Асинхронный генератор
Python 3.5; async/await
Тихо и незаметно (с), вышел 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
- Другая сопрограмма, а именно объект native coroutine. Это напоминает, и видимо реализовано аналогично случаю, когда в генераторе с помощью yield from вызывается другой генератор.
- Сопрограмма на основе генератора, созданная с помощью декоратора types.coroutine(). Это вариант обеспечения совместимости с наработками, где сопрограммы реализованы на основе генераторов.
- Специальный объект, у которого реализован магический метод __await__, возвращающий итератор. С помощью этого итератора реализуется возврат результата выполнения сопрограммы.
async
- async def — определяет native coroutine function, результатом вызова которой будет объект-сопрограмма native coroutine, пока еще не запущенная.
- async for — определяет, что итератор используемый в цикле, при получении следующего значения может переключать выполнение с текущей сопрограммы. Объект итератор имеет вместо стандартных магических методов: __iter__ и __next__, методы: __aiter__ и __anext__. Функционально они аналогичны, но как следует из определения, допускают использования await в своем теле.
- async with — определяет, что при входе в контекстный блок и выходе из него может быть переключение выполнения с текущей сопрограммы. Так же, как и в случае с асинхронным генератором, вместо магических методов: __enter__ и __exit__ следует использовать функционально аналогичные __aenter__ и __aexit__.
Примеры на использование асинхронных итераторов и контекст менеджеров в документации и 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 методы асинхронного программирования.
Использование async/await в Python
Почитал про использование async / await в других языках программирования и не совсем понял, как и когда их используют. Для чего они вообще нужны? С помощью них можно улучшить уже существующий код? В каких случаях не стоит их использовать?
PEP 492 introduced support for native coroutines and async / await syntax to Python 3.5. A notable limitation of the Python 3.5 implementation is that it was not possible to use await and yield in the same function body. In Python 3.6 this restriction has been lifted, making it possible to define asynchronous generators:
async def ticker(delay, to): """Yield numbers from 0 to *to* every *delay* seconds.""" for i in range(to): yield i await asyncio.sleep(delay)
PEP 530 adds support for using async for in list, set, dict comprehensions and generator expressions:
result = [i async for i in aiter() if i % 2]
Additionally, await expressions are supported in all kinds of comprehensions:
result = [await fun() for fun in funcs if await condition()]
Отслеживать
задан 24 дек 2016 в 22:20
Trajectory Trajectory
181 3 3 золотых знака 4 4 серебряных знака 11 11 бронзовых знаков
1 ответ 1
Сортировка: Сброс на вариант по умолчанию
Смотрите. Async/await нужен для того, чтобы не блокировать поток выполнения на время ожидания какого-нибудь асинхронного события. Конструкция Async/await превращает по сути процедуру в корутину (сопрограмму): она прекращает своё выполнение на время await , дожидается асинхронного события, и возобновляет работу.
В не-async-варианте ожидание получается блокирующим, или нужно вручную делать трюки: запускать операцию и подписываться на её окончание. Async делает код более простым, линейным.
Пример (на псевдокоде):
DownloadToFile(url): filename = GetFilename() content = await DownloadUrl(url) WriteToFile(filename, content) ReportSuccess()
DownloadToFile(url): filename = GetFilename() BeginDownloadUrl(url, onfinished: lambda content: StoreContent(content, filename)) StoreContent(content, filename) WriteToFile(filename, content) ReportSuccess()
Вы видите, что без async контекст выполнения (локальные переменные и т. п.) приходится передавать в «хвост» функции (continuation) вручную. Если async-вызовов много, аналогичный код без async быстро становится сложным.
Важное отличие async- от синхронной функции — async-функция возвращается к вызывающему коду в момент первого выполнения await (если тот ещё не завершён). Вызывающий код может дождаться полного окончания работы при помощи await, а может и продолжить работу самостоятельно.
Использование async/await имеет смысл там, где у вас есть ожидание, не связанное с нагрузкой на процессор. Например, ожидание прихода данных из интернета, или чтения файла с диска. В этом случае вы освобождаете физический поток выполнения для системы, но логическое выполнение продолжается (после возврата из await ).