Переход с JavaScript на Python напоминает переезд в город, где дорожные знаки те же, а правила дорожного движения — другие. Синтаксис кажется дружелюбным и знакомым. Вы видите async и await прямо в грамматике языка и предполагаете, что ментальная модель перенесется без изменений. Но это не так. Один привычный паттерн из JavaScript незаметно обрушит производительность вашего Python-кода, не вызывая ошибок, не оставляя логов и не бросаясь в глаза при быстром ревью.
Как JavaScript учит вас «запустить и забыть»
В JavaScript вызов асинхронной функции является «жадным» (eager). Как только вы вызываете её, движок создает Promise, и работа начинается немедленно. Event loop уже вовсю работает. Вот почему разработчики на JavaScript естественным образом пишут код вот так:
const userPromise = fetchUser(id);
const ordersPromise = fetchOrders(id);
const user = await userPromise;
const orders = await ordersPromise;
Оба сетевых запроса уже находятся в процессе выполнения еще до того, как будет достигнут первый await. Первый await приостанавливает текущую функцию до тех пор, пока fetchUser не завершится, но fetchOrders уже вовсю работает в фоновом режиме с предыдущей строки. К тому времени, когда вам понадобится переменная orders, второй запрос может быть уже завершен. Этот паттерн кажется настолько естественным в JavaScript, что многие разработчики даже не рассматривают его как трюк для конкурентности. Это просто то, как работает async.
Сюрприз Python: «холодная» корутина
Python использует другой контракт. Когда вы вызываете функцию, объявленную через async def, в Python, вы не запускаете никакую работу. Вы получаете объект корутины. Представьте, что это рецепт, записанный на бумаге. Ингредиенты перечислены, шаги понятны, но в духовке ничего не готовится. Пока что-то явно не прогонит эту корутину через event loop, она будет оставаться инертной.
Вот в чем ловушка. Инженер, привыкший к JavaScript, которому нужны данные пользователя и его заказы, может написать на Python следующее:
user_coro = fetch_user(id)
orders_coro = fetch_orders(id)
user = await user_coro
orders = await orders_coro
Это выглядит как конкурентность. Это ощущается как конкурентность. Но на самом деле это чистая последовательность.
Первая строка присваивает спящую корутину переменной user_coro. Вторая строка присваивает еще одну спящую корутину переменной orders_coro. Когда выполнение доходит до await user_coro, Python наконец-то запускает первую задачу и выполняет её до конца. Только после завершения fetch_user интерпретатор доходит до await orders_coro и запускает вторую задачу. Общее время выполнения будет равно сумме обеих операций ввода-вывода, а не длительности самой долгой из них. Вы не запустили их параллельно. Вы запустили их одну за другой, да еще и с лишними действиями.
Почему этот баг невидим
Это тот тип регрессии производительности, который может существовать месяцами. Код является валидным Python. Он проходит проверки типов. Он возвращает правильные результаты. Он просто работает в два раза медленнее или даже хуже. Поскольку нет ни трассировки стека, ни предупреждений, инженерные команды часто ищут проблему в других местах. Они добавляют кэши Redis, обновляют уровни баз данных или меняют регионы хостинга. Настоящий виновник — тонкое несовпадение ожиданий относительно того, что на самом деле делает await.
Три способа заставить Python действительно выполнять задачи конкурентно
Чтобы исправить это, вы должны приказать event loop в Python запланировать работу немедленно. Вам нужно что-то более активное, чем просто корутина. Вам нужна Задача (Task).
1. asyncio.create_task
Самый прямой перевод паттерна из JavaScript — это обернуть корутину в Task. Task планируется в event loop сразу же после создания. Это ближайший эквивалент JavaScript Promise, который уже находится в движении.
user_task = asyncio.create_task(fetch_user(id))
orders_task = asyncio.create_task(fetch_orders(id))
user = await user_task
orders = await_orders_task
Теперь и fetch_user, и fetch_orders находятся в процессе выполнения еще до первого await. Когда вы доходите до await user_task, вы приостанавливаетесь только до завершения этой конкретной задачи, но другая задача продолжает работать. Если fetch_orders завершится первым, его результат просто будет ждать внутри orders_task, пока вы его не запросите.
Однако будьте осторожны. Если вы создадите Task и никогда не примените к нему await, Python выдаст ошибку о том, что незавершенная задача была уничтожена (destroyed pending task). Вам все равно нужно собрать результаты.
2. asyncio.gather
Если у вас есть несколько корутин, которые должны завершиться, прежде чем вы пойдете дальше, asyncio.gather возьмет на себя всю рутину. Внутри он планирует каждую корутину как Task и ожидает их завершения вместе.
user, orders = await asyncio.gather(fetch_user(id), fetch_orders(id))
Это лаконично и читаемо. Этот метод идеален, когда операции независимы и вы хотите одной строкой выразить: «выполни всё это, а затем отдай мне каждый результат». Он также сохраняет порядок аргументов в возвращаемом списке или кортеже, даже если базовые задачи завершаются в другом порядке.
3. asyncio.TaskGroup
Python 3.11 представил TaskGroup, который привносит структурированную конкурентность в стандартную библиотеку. Вместо того чтобы создавать задачи вручную, вы используете контекстный менеджер, который гарантирует правильное завершение каждой созданной задачи. Если одна задача вызывает исключение, остальные отменяются автоматически.
async with asyncio.TaskGroup() as tg:
user_task = tg.create_task(fetch_user(id))
orders_task = tg.create_task(fetch_orders(id))
user = user_task.result()
orders = orders_task.result()
Этот паттерн отлично подходит для сложных рабочих процессов. Он устраняет риск появления «задач-сирот» и объединяет жизненный цикл связанных операций под одним логическим зонтиком. Если ваш код работает на Python 3.11 или новее, это зачастую самая чистая архитектура для конкурентности типа fan-out.
Ментальная модель: await означает «выполни это сейчас»
Основной урок заключается в лингвистике. В JavaScript await можно воспринимать как «тем временем». Вы запускаете работу, занимаетесь другими делами и ставите выполнение на паузу только тогда, когда вам нужно значение. В Python await означает «доведи эту корутину до следующей точки приостановки или до завершения». Если корутина еще не была запланирована, именно await планирует её выполнение. Вот почему нельзя просто запустить две «сырые» корутины, а затем вызвать await для них позже. Вы не дали циклу событий (event loop) никакой работы на это время.
Представьте корутины Python как функции-генераторы. Вызов генератора не запускает итерацию. Вам нужно пройтись по нему циклом, вызвать next() или передать его потребителю. Асинхронность работает так же. asyncio.create_task — это потребитель, который говорит: «помести это в цикл событий прямо сейчас». Последующий await просто ждет сигнала о завершении.
Одна полезная привычка: всякий раз, когда вы присваиваете результат вызова асинхронной функции переменной без использования await, спросите себя, запланировали ли вы её выполнение. Если правая часть выражения не обернута в create_task, gather или TaskGroup, она не выполняется. Это всего лишь «рецепт», лежащий на столе.
Итог
Среда выполнения асинхронного кода в Python мощная, но она требует явного намерения. Язык не начинает фоновую работу только потому, что вы вызвали функцию. Если вы переходите с JavaScript, проверьте каждое место, где вы сохраняете корутину в переменную, чтобы вызвать await позже. Если вы предварительно не превратили её в Task, вы написали последовательный код, просто «одетый» в асинхронную оболочку. Запускайте работу через Task, а затем ждите результатов. Именно так вы превратите асинхронность Python из скрытого «узкого места» в настоящий инструмент параллелизма.
