Сопрограммы/корутины 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
Когда говорят «написать корутину», обычно подразумевают асинхронную функцию. Корутины можно ставить на паузу, чтобы дать другим функциям немного поработать. В этом заключается принцип асинхронности. О нём мы рассказывали в этой статье.
Давайте сразу рассмотрим пример асинхронной функции:
import asyncio async def count_to_three(): print("Веду отсчёт. 1") await asyncio.sleep(0) print("Веду отсчёт. 2") await asyncio.sleep(0) print("Веду отсчёт. 3") await asyncio.sleep(0)
Очень похоже на обычную функцию, однако здесь есть два новых слова: async и await .
async говорит Питону о том, что мы пишем не просто функцию, а асинхронную функцию. Просто добавили async и всё, функция теперь асинхронная.
Второе слово — await. Оно прерывает исполнение функции, и возвращает управление программой наружу. После этого корутину можно запустить повторно, а затем еще и еще, и каждый раз она будет продолжать работу с того await , на котором прервалась ранее. Например, в функции count_to_three команда await встречается три раза, значит корутину можно вызвать четыре раза (да, не три!). Корутина будет работать до первого await, затем до второго, до третьего и на четвёртый раз выполнит остатки до конца.
Нельзя делать await None или await «Hello, World!» . Можно await только то, что так и называют — «awaitable».
await asyncio.sleep(0) — это команда корутине «Дай поработать другим!»
Сразу покажем, как это выглядит на практике:
coroutine_counter = count_to_three() print(coroutine_counter) # coroutine_counter.send(None) # Выведет "Веду отсчёт. 1" coroutine_counter.send(None) # Выведет "Веду отсчёт. 2" coroutine_counter.send(None) # Выведет "Веду отсчёт. 3"
Мы вызываем асинхронную функцию count_to_three , однако она не выводит на экран цифру 1, а возвращает корутину. Все асинхронные функции так делают. Это сделано для того, чтобы у вас был объект этой корутины в переменной. Теперь корутину можно запускать раз за разом, а она раз за разом будет делать кусочек и останавливаться на следующем await .
Чтобы запустить корутину, используют метод send() . При каждом запуске корутины этим методом она продолжает исполняться с последнего await , на котором она остановилась. Поэтому при новом запуске той же корутины срабатывает не тот же print , а следующий.
Нельзя просто .send() . Всегда нужно передавать какое-то значение. Об этом тоже расскажем позже. Пока что воспринимайте .send(None) как команду «продолжи выполнять корутину».
Когда корутина закончится?
Она остановится навсегда, когда закончатся все await или встретится return . Когда корутина заканчивается — она истощается и вызов .send() выдаёт ошибку:
coroutine_counter = count_to_three() coroutine_counter.send(None) # Выведет "Веду отсчёт. 1" coroutine_counter.send(None) # Выведет "Веду отсчёт. 2" coroutine_counter.send(None) # Выведет "Веду отсчёт. 3" coroutine_counter.send(None) # Выбросит ошибку StopIteration
Если мы хотим запустить наш счётчик сначала, придётся создать новую корутину, вызвав count_to_three() :
coroutine_counter = count_to_three() coroutine_counter.send(None) # Выведет "Веду отсчёт. 1" coroutine_counter.send(None) # Выведет "Веду отсчёт. 2" coroutine_counter_new = count_to_three() coroutine_counter_new.send(None) # Снова выведет "Веду отсчёт. 1", новая корутина
Обычно заранее не известно сколько await будет до момента «истощения», поэтому исключение приходится «перехватывать»:
coroutine_counter = count_to_three() while True: try: coroutine_counter.send(None) # В четвёртый раз здесь вылетит StopIteration except StopIteration: break
Исключение StopIteration возникает всего один раз. Если после него попробовать запустить корутину ещё раз, то поднимется другое исключение — RuntimeError , и оно уже будет считаться ошибкой. О том как работать с исключениями читайте в статье про try except.
Нельзя запускать истощённую корутину.
Добиваемся асинхронности
С корутинами разобрались, останавливать их научились. А зачем.
Корутины позволят вашему коду работать асинхронно, т.е. делать несколько вещей одновременно. Допустим, вы решили скачать несколько файлов. Обычный, синхронный код скачивает файлы по-очереди. Сначала первый файл целиком, затем второй, тоже целиком. Асинхронный код качает файлы одновременно, по кусочкам. Приведём пример скачивания двух файлов:
async def download_file(url): # здесь происходит какая-то логика со скачиванием файла image_downloader = download_file('https://www.some-images.com/image1.jpg') music_downloader = download_file('https://www.music-site.com/artist/album/song5.mp3') coroutines = [music_downloader, image_downloader] while True: for coroutine in coroutines.copy(): try: coroutine.send(None) except StopIteration: coroutines.remove(coroutine) if len(coroutines) == 0: break
Разберём как работает код:
- Мы создали 2 корутины: image_downloader и music_downloader . Первая качает картинку по ссылке https://www.some-images.com/image1.jpg , вторая — музыку по ссыке https://www.music-site.com/artist/album/song5.mp3 .
- Мы положили их в список coroutines
- В бесконечном цикле мы по очереди запускаем все корутины из списка. Если вышла ошибка StopIteration — корутина истощилась, т.е. файл скачан. Убираем её из списка, корутина больше запускаться не будет.
- Чтобы итерация по списку coroutines не сбивалась после удаления элемента из него итерируем не по оригиналу, а по копии coroutines.copy() .
- Если список с корутинами закончился (его длина равна нулю), пора заканчивать и бесконечный цикл, потому что все файлы скачаны.
Передать параметры в асинхронную функцию
В плане аргументов асинхронные функции ничем не отличаются от обычных. Доработаем пример со счетчиком и вместо async def count_to_three напишем универсальную функцию async def count :
import asyncio async def count(limit=3): for step in range(1, limit+1): print("Веду отсчёт.", step) await asyncio.sleep(0) coroutine = count(5) while True: coroutine.send(None)
Веду отсчёт. 1 Веду отсчёт. 2 Веду отсчёт. 3 Веду отсчёт. 4 Веду отсчёт. 5 Traceback (most recent call last): File "", line 2, in StopIteration
Попробуйте бесплатные уроки по 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 ).
Конкурентность и async / await¶
Здесь приведена подробная информация об использовании синтаксиса async def при написании функций обработки пути, а также рассмотрены основы асинхронного программирования, конкурентности и параллелизма.
Нет времени? ¶
TL;DR:
Допустим, вы используете сторонюю библиотеку, которая требует вызова с ключевым словом await :
results = await some_library()
В этом случае функции обработки пути необходимо объявлять с использованием синтаксиса async def :
@app.get('/') async def read_results(): results = await some_library() return results
await можно использовать только внутри функций, объявленных с использованием async def .
Если вы обращаетесь к сторонней библиотеке, которая с чем-то взаимодействует (с базой данных, API, файловой системой и т. д.), и не имеет поддержки синтаксиса await (что относится сейчас к большинству библиотек для работы с базами данных), то объявляйте функции обработки пути обычным образом с помощью def , например:
@app.get('/') def results(): results = some_library() return results
Если вашему приложению (странным образом) не нужно ни с чем взаимодействовать и, соответственно, ожидать ответа, используйте async def .
Если вы не уверены, используйте обычный синтаксис def .
Примечание: при необходимости можно смешивать def и async def в функциях обработки пути и использовать в каждом случае наиболее подходящий синтаксис. А FastAPI сделает с этим всё, что нужно.
В любом из описанных случаев FastAPI работает асинхронно и очень быстро.
Однако придерживаясь указанных советов, можно получить дополнительную оптимизацию производительности.
Технические подробности¶
Современные версии Python поддерживают разработку так называемого «асинхронного кода» посредством написания «сопрограмм» с использованием синтаксиса async и await .
Ниже разберём эту фразу по частям:
Асинхронный код¶
Асинхронный код означает, что в языке есть возможность сообщить машине / программе , что в определённой точке кода ей нужно будет ожидать завершения выполнения чего-то ещё в другом месте. Допустим это что-то ещё называется «медленный файл» .
И пока мы ждём завершения работы с «медленным файлом» , компьютер может переключиться для выполнения других задач.
Но при каждой возможности компьютер / программа будет возвращаться обратно. Например, если он опять окажется в режиме ожидания, или когда закончит всю работу. В этом случае компьютер проверяет, не завершена ли какая-нибудь из текущих задач.
Потом он берёт первую выполненную задачу (допустим, наш «медленный файл» ) и продолжает работу, производя с ней необходимые действия.
Вышеупомянутое «что-то ещё», завершения которого приходится ожидать, обычно относится к достаточно «медленным» операциям I/O (по сравнению со скоростью работы процессора и оперативной памяти), например:
- отправка данных от клиента по сети
- получение клиентом данных, отправленных вашей программой по сети
- чтение системой содержимого файла с диска и передача этих данных программе
- запись на диск данных, которые программа передала системе
- обращение к удалённому API
- ожидание завершения операции с базой данных
- получение результатов запроса к базе данных
- и т. д.
Поскольку в основном время тратится на ожидание выполнения операций I/O , их обычно называют операциями, ограниченными скоростью ввода-вывода .
Код называют «асинхронным», потому что компьютеру / программе не требуется «синхронизироваться» с медленной задачей и, будучи в простое, ожидать момента её завершения, с тем чтобы забрать результат и продолжить работу.
Вместо этого в «асинхронной» системе завершённая задача может немного подождать (буквально несколько микросекунд), пока компьютер / программа занимается другими важными вещами, с тем чтобы потом вернуться, забрать результаты выполнения и начать их обрабатывать.
«Синхронное» исполнение (в противовес «асинхронному») также называют «последовательным» , потому что компьютер / программа последовательно выполняет все требуемые шаги перед тем, как перейти к следующей задаче, даже если в процессе приходится ждать.
Конкурентность и бургеры¶
Тот асинхронный код, о котором идёт речь выше, иногда называют «конкурентностью». Она отличается от «параллелизма».
Да, конкурентность и параллелизм подразумевают, что разные вещи происходят примерно в одно время.
Но внутреннее устройство конкурентности и параллелизма довольно разное.
Чтобы это понять, представьте такую картину:
Конкурентные бургеры¶
Вы идёте со своей возлюбленной в фастфуд и становитесь в очередь, в это время кассир принимает заказы у посетителей перед вами.
Когда наконец подходит очередь, вы заказываете парочку самых вкусных и навороченных бургеров , один для своей возлюбленной , а другой себе.
Кассир что-то говорит поварам на кухне , теперь они знают, какие бургеры нужно будет приготовить (но пока они заняты бургерами предыдущих клиентов).
Кассир отдаёт вам чек с номером заказа.
В ожидании еды вы идёте со своей возлюбленной выбрать столик, садитесь и довольно продолжительное время общаетесь (поскольку ваши бургеры самые навороченные, готовятся они не так быстро ✨✨).
Сидя за столиком с возлюбленной в ожидании бургеров , вы отлично проводите время, восхищаясь её великолепием, красотой и умом ✨✨.
Всё ещё ожидая заказ и болтая со своей возлюбленной , время от времени вы проверяете, какой номер горит над прилавком, и не подошла ли уже ваша очередь.
И вот наконец настаёт этот момент, и вы идёте к стойке, чтобы забрать бургеры и вернуться за столик.
Вы со своей возлюбленной едите бургеры и отлично проводите время ✨.
А теперь представьте, что в этой небольшой истории вы компьютер / программа .
В очереди вы просто глазеете по сторонам , ждёте и ничего особо «продуктивного» не делаете. Но очередь движется довольно быстро, поскольку кассир только принимает заказы (а не занимается приготовлением еды), так что ничего страшного.
Когда подходит очередь вы наконец предпринимаете «продуктивные» действия : просматриваете меню, выбираете в нём что-то, узнаёте, что хочет ваша возлюбленная , собираетесь оплатить , смотрите, какую достали карту, проверяете, чтобы с вас списали верную сумму, и что в заказе всё верно и т. д.
И хотя вы всё ещё не получили бургеры , ваша работа с кассиром ставится «на паузу» ⏸, поскольку теперь нужно ждать , когда заказ приготовят.
Но отойдя с номерком от прилавка, вы садитесь за столик и можете переключить внимание на свою возлюбленную и «работать» ⏯ уже над этим. И вот вы снова очень «продуктивны» , мило болтаете вдвоём и всё такое .
В какой-то момент кассир поместит на табло ваш номер, подразумевая, что бургеры готовы , но вы не станете подскакивать как умалишённый, лишь только увидев на экране свою очередь. Вы уверены, что ваши бургеры никто не утащит, ведь у вас свой номерок, а у других свой.
Поэтому вы подождёте, пока возлюбленная закончит рассказывать историю (закончите текущую работу ⏯ / задачу в обработке ), и мило улыбнувшись, скажете, что идёте забирать заказ ⏸.
И вот вы подходите к стойке , к первоначальной задаче, которая уже завершена ⏯, берёте бургеры , говорите спасибо и относите заказ за столик. На этом заканчивается этап / задача взаимодействия с кассой ⏹. В свою очередь порождается задача «поедание бургеров» ⏯, но предыдущая («получение бургеров») завершена ⏹.
Параллельные бургеры¶
Теперь представим, что вместо бургерной «Конкурентные бургеры» вы решили сходить в «Параллельные бургеры».
И вот вы идёте со своей возлюбленной отведать параллельного фастфуда .
Вы становитесь в очередь пока несколько (пусть будет 8) кассиров, которые по совместительству ещё и повары , принимают заказы у посетителей перед вами.
При этом клиенты не отходят от стойки и ждут получения еды, поскольку каждый из 8 кассиров идёт на кухню готовить бургеры , а только потом принимает следующий заказ.
Наконец настаёт ваша очередь, и вы просите два самых навороченных бургера , один для дамы сердца , а другой себе.
Ни о чём не жалея, расплачиваетесь .
И кассир уходит на кухню .
Вам приходится ждать перед стойкой , чтобы никто по случайности не забрал ваши бургеры , ведь никаких номерков у вас нет.
Поскольку вы с возлюбленной хотите получить заказ вовремя , и следите за тем, чтобы никто не вклинился в очередь, у вас не получается уделять должного внимание своей даме сердца .
Это «синхронная» работа, вы «синхронизированы» с кассиром/поваром . Приходится ждать у стойки, когда кассир/повар закончит делать бургеры и вручит вам заказ, иначе его случайно может забрать кто-то другой.
Наконец кассир/повар возвращается с бургерами после невыносимо долгого ожидания за стойкой.
Вы скорее забираете заказ и идёте с возлюбленной за столик.
Там вы просто едите эти бургеры, и на этом всё ⏹.
Вам не особо удалось пообщаться, потому что большую часть времени пришлось провести у кассы .
В описанном сценарии вы компьютер / программа с двумя исполнителями (вы и ваша возлюбленная ), на протяжении долгого времени вы оба уделяете всё внимание ⏯ задаче «ждать на кассе».
В этом ресторане быстрого питания 8 исполнителей (кассиров/поваров) . Хотя в бургерной конкурентного типа было всего два (один кассир и один повар) .
Несмотря на обилие работников, опыт в итоге получился не из лучших .
Так бы выглядел аналог истории про бургерную в «параллельном» мире.
Вот более реалистичный пример. Представьте себе банк.
До недавних пор в большинстве банков было несколько кассиров и длинные очереди .
Каждый кассир обслуживал одного клиента, потом следующего ⏯.
Нужно было долгое время стоять перед окошком вместе со всеми, иначе пропустишь свою очередь.
Сомневаюсь, что у вас бы возникло желание прийти с возлюбленной в банк оплачивать налоги.
Выводы о бургерах¶
В нашей истории про поход в фастфуд за бургерами приходится много ждать , поэтому имеет смысл организовать конкурентную систему ⏸⏯.
И то же самое с большинством веб-приложений.
Пользователей очень много, но ваш сервер всё равно вынужден ждать запросы по их слабому интернет-соединению.
Потом снова ждать , пока вернётся ответ.
Это ожидание измеряется микросекундами, но если всё сложить, то набегает довольно много времени.
Вот почему есть смысл использовать асинхронное ⏸⏯ программирование при построении веб-API.
Большинство популярных фреймворков (включая Flask и Django) создавались до появления в Python новых возможностей асинхронного программирования. Поэтому их можно разворачивать с поддержкой параллельного исполнения или асинхронного программирования старого типа, которое не настолько эффективно.
При том, что основная спецификация асинхронного взаимодействия Python с веб-сервером (ASGI) была разработана командой Django для внедрения поддержки веб-сокетов.
Именно асинхронность сделала NodeJS таким популярным (несмотря на то, что он не параллельный), и в этом преимущество Go как языка программирования.
И тот же уровень производительности даёт FastAPI.
Поскольку можно использовать преимущества параллелизма и асинхронности вместе, вы получаете производительность лучше, чем у большинства протестированных NodeJS фреймворков и на уровне с Go, который является компилируемым языком близким к C (всё благодаря Starlette).
Получается, конкурентность лучше параллелизма?¶
Нет! Мораль истории совсем не в этом.
Конкурентность отличается от параллелизма. Она лучше в конкретных случаях, где много времени приходится на ожидание. Вот почему она зачастую лучше параллелизма при разработке веб-приложений. Но это не значит, что конкурентность лучше в любых сценариях.
Давайте посмотрим с другой стороны, представьте такую картину:
Вам нужно убраться в большом грязном доме.
Да, это вся история.
Тут не нужно нигде ждать , просто есть куча работы в разных частях дома.
Можно организовать очередь как в примере с бургерами, сначала гостиная, потом кухня, но это ни на что не повлияет, поскольку вы нигде не ждёте , а просто трёте да моете.
И понадобится одинаковое количество времени с очередью (конкурентностью) и без неё, и работы будет сделано тоже одинаковое количество.
Однако в случае, если бы вы могли привести 8 бывших кассиров/поваров, а ныне уборщиков , и каждый из них (вместе с вами) взялся бы за свой участок дома, с такой помощью вы бы закончили намного быстрее, делая всю работу параллельно.
В описанном сценарии каждый уборщик (включая вас) был бы исполнителем, занятым на своём участке работы.
И поскольку большую часть времени выполнения занимает реальная работа (а не ожидание), а работу в компьютере делает ЦП , такие задачи называют ограниченными производительностью процессора .
Ограничение по процессору проявляется в операциях, где требуется выполнять сложные математические вычисления.
- Обработка звука или изображений.
- Компьютерное зрение: изображение состоит из миллионов пикселей, в каждом пикселе 3 составляющих цвета, обработка обычно требует проведения расчётов по всем пикселям сразу.
- Машинное обучение: здесь обычно требуется умножение «матриц» и «векторов». Представьте гигантскую таблицу с числами в Экселе, и все их надо одновременно перемножить.
- Глубокое обучение: это область машинного обучения, поэтому сюда подходит то же описание. Просто у вас будет не одна таблица в Экселе, а множество. В ряде случаев используется специальный процессор для создания и / или использования построенных таким образом моделей.
Конкурентность + параллелизм: Веб + машинное обучение¶
FastAPI предоставляет возможности конкуретного программирования, которое очень распространено в веб-разработке (именно этим славится NodeJS).
Кроме того вы сможете использовать все преимущества параллелизма и многопроцессорности (когда несколько процессов работают параллельно), если рабочая нагрузка предполагает ограничение по процессору, как, например, в системах машинного обучения.
Необходимо также отметить, что Python является главным языком в области дата-сайенс , машинного обучения и, особенно, глубокого обучения. Всё это делает FastAPI отличным вариантом (среди многих других) для разработки веб-API и приложений в области дата-сайенс / машинного обучения.
Как добиться такого параллелизма в эксплуатации описано в разделе Развёртывание.
async и await ¶
В современных версиях Python разработка асинхронного кода реализована очень интуитивно. Он выглядит как обычный «последовательный» код и самостоятельно выполняет «ожидание», когда это необходимо.
Если некая операция требует ожидания перед тем, как вернуть результат, и поддерживает современные возможности Python, код можно написать следующим образом:
burgers = await get_burgers(2)
Главное здесь слово await . Оно сообщает интерпретатору, что необходимо дождаться ⏸ пока get_burgers(2) закончит свои дела , и только после этого сохранить результат в burgers . Зная это, Python может пока переключиться на выполнение других задач ⏯ (например получение следующего запроса).
Чтобы ключевое слово await сработало, оно должно находиться внутри функции, которая поддерживает асинхронность. Для этого вам просто нужно объявить её как async def :
async def get_burgers(number: int): # Готовим бургеры по специальному асинхронному рецепту return burgers
# Это не асинхронный код def get_sequential_burgers(number: int): # Готовим бургеры последовательно по шагам return burgers
Объявление async def указывает интерпретатору, что внутри этой функции следует ожидать выражений await , и что можно поставить выполнение такой функции на «паузу» ⏸ и переключиться на другие задачи , с тем чтобы вернуться сюда позже.
Если вы хотите вызвать функцию с async def , вам нужно «ожидать» её. Поэтому такое не сработает:
# Это не заработает, поскольку get_burgers объявлена с использованием async def burgers = get_burgers(2)
Если сторонняя библиотека требует вызывать её с ключевым словом await , необходимо писать функции обработки пути с использованием async def , например:
@app.get('/burgers') async def read_burgers(): burgers = await get_burgers(2) return burgers
Технические подробности¶
Как вы могли заметить, await может применяться только в функциях, объявленных с использованием async def .
Но выполнение такой функции необходимо «ожидать» с помощью await . Это означает, что её можно вызвать только из другой функции, которая тоже объявлена с async def .
Но как же тогда появилась первая курица ? В смысле. как нам вызвать первую асинхронную функцию?
При работе с FastAPI просто не думайте об этом, потому что «первой» функцией является ваша функция обработки пути, и дальше с этим разберётся FastAPI.
Кроме того, если хотите, вы можете использовать синтаксис async / await и без FastAPI.
Пишите свой асинхронный код¶
Starlette (и FastAPI) основаны на AnyIO, что делает их совместимыми как со стандартной библиотекой asyncio в Python, так и с Trio.
В частности, вы можете напрямую использовать AnyIO в тех проектах, где требуется более сложная логика работы с конкурентностью.
Даже если вы не используете FastAPI, вы можете писать асинхронные приложения с помощью AnyIO, чтобы они были максимально совместимыми и получали его преимущества (например структурную конкурентность).
Другие виды асинхронного программирования¶
Стиль написания кода с async и await появился в языке Python относительно недавно.
Но он сильно облегчает работу с асинхронным кодом.
Ровно такой же синтаксис (ну или почти такой же) недавно был включён в современные версии JavaScript (в браузере и NodeJS).
До этого поддержка асинхронного кода была реализована намного сложнее, и его было труднее воспринимать.
В предыдущих версиях Python для этого использовались потоки или Gevent. Но такой код намного сложнее понимать, отлаживать и мысленно представлять.
Что касается JavaScript (в браузере и NodeJS), раньше там использовали для этой цели «обратные вызовы» . Что выливалось в ад обратных вызовов.
Сопрограммы¶
Корути́на (или же сопрограмма) — это крутое словечко для именования той сущности, которую возвращает функция async def . Python знает, что её можно запустить, как и обычную функцию, но кроме того сопрограмму можно поставить на паузу ⏸ в том месте, где встретится слово await .
Всю функциональность асинхронного программирования с использованием async и await часто обобщают словом «корутины». Они аналогичны «горутинам» , ключевой особенности языка Go.
Заключение¶
В самом начале была такая фраза:
Современные версии Python поддерживают разработку так называемого «асинхронного кода» посредством написания «сопрограмм» с использованием синтаксиса async и await .
Теперь всё должно звучать понятнее. ✨
На этом основана работа FastAPI (посредством Starlette), и именно это обеспечивает его высокую производительность.
Очень технические подробности¶
Этот раздел читать не обязательно.
Здесь приводятся подробности внутреннего устройства FastAPI.
Но если вы обладаете техническими знаниями (корутины, потоки, блокировка и т. д.) и вам интересно, как FastAPI обрабатывает async def в отличие от обычных def , читайте дальше.
Функции обработки пути¶
Когда вы объявляете функцию обработки пути обычным образом с ключевым словом def вместо async def , FastAPI ожидает её выполнения, запустив функцию во внешнем пуле потоков , а не напрямую (это бы заблокировало сервер).
Если ранее вы использовали другой асинхронный фреймворк, который работает иначе, и привыкли объявлять простые вычислительные функции через def ради незначительного прироста скорости (порядка 100 наносекунд), обратите внимание, что с FastAPI вы получите противоположный эффект. В таком случае больше подходит async def , если только функция обработки пути не использует код, приводящий к блокировке I/O .
Но в любом случае велика вероятность, что FastAPI окажется быстрее другого фреймворка (или хотя бы на уровне с ним).
Зависимости¶
То же относится к зависимостям. Если это обычная функция def , а не async def , она запускается во внешнем пуле потоков.
Подзависимости¶
Вы можете объявить множество ссылающихся друг на друга зависимостей и подзависимостей (в виде параметров при определении функции). Какие-то будут созданы с помощью async def , другие обычным образом через def , и такая схема вполне работоспособна. Функции, объявленные с помощью def будут запускаться на внешнем потоке (из пула), а не с помощью await .
Другие служебные функции¶
Любые другие служебные функции, которые вы вызываете напрямую, можно объявлять с использованием def или async def . FastAPI не будет влиять на то, как вы их запускаете.
Этим они отличаются от функций, которые FastAPI вызывает самостоятельно: функции обработки пути и зависимости.
Если служебная функция объявлена с помощью def , она будет вызвана напрямую (как вы и написали в коде), а не в отдельном потоке. Если же она объявлена с помощью async def , её вызов должен осуществляться с ожиданием через await .
Ещё раз повторим, что все эти технические подробности полезны, только если вы специально их искали.
В противном случае просто ознакомьтесь с основными принципами в разделе выше: Нет времени?.