Первая корутина: async def и await
async def — функция с паузами, await — «дождись результата». Звучит просто, пока не выясняется, что вызов async-функции не выполняет её тело. Разбираем оба ключевых слова на живом коде.
Редакция Питоники
Обычная функция в Python устроена честно: вызвал — выполнилась — вернула результат. С асинхронными всё хитрее, и именно здесь новичков ждёт первый сюрприз: fetch("Каталог") выглядит как вызов функции, а тело её при этом не выполняется ни на строчку. В этом уроке разберём оба новых ключевых слова по отдельности — сначала async def, потом await, — и договоримся, как запускать корутины правильно, чтобы никогда не оказаться в ситуации «написал вызов, а вывода нет».
async def: функция с паузами
Ключевое слово async перед def помечает функцию как корутину. Корутина — это функция, которая умеет приостанавливаться: дойти до строки с await, отдать управление и продолжиться с того же места, когда придёт время. Пока достаточно этой картинки, а что стоит за «отдать управление» — механизм событийного цикла — разберём в уроке 3. Вот минимальная корутина и её правильный запуск:
import asyncio
async def fetch(name):
return f"{name}: готово"
async def main():
result = await fetch("Каталог")
print(result)
asyncio.run(main())
Каталог: готово
Разбираем по ролям. async def fetch(name) — объявление корутины: функция получила право на паузы, даже если в ней ни одной паузы и нет, как здесь. async def main() — вторая корутина, из которой мы будем запускать другие. result = await fetch("Каталог") — правильный способ «вызвать» корутину: await запускает её тело, дожидается return и кладёт результат в переменную. asyncio.run(main()) — точка входа: она раскручивает main, а та — всё остальное. Почему нельзя просто вызвать fetch("Каталог") и ждать результата — сейчас увидим.
Пара слов про имена, пока синтаксис свежий. Корутины называют глаголами — fetch, load, save: они обозначают действия, которые произойдут, когда корутину запустят. А main — не зарезервированное слово, а соглашение: точка входа, из которой расходятся остальные. Через несколько уроков в main появятся задачи и пачки, но шаблон «объявила — запустила» останется тем же.
И ещё одна деталь, которую легко упустить: async def без единого await внутри — законная корутина. Она исправно создаётся и запускается, просто никогда не уступает управление — и в этом смысле ведёт себя как обычная функция с лишним словом. Паузы делают корутину корутиной, а не строка async; если в теле нечего ждать, спроси себя, зачем ей вообще асинхронность.
Вызов без await: что возвращается на самом деле
Проверим честно: что лежит в переменной, если корутину «вызвать» без await. Для проверки возьмём inspect.iscoroutine из стандартной библиотеки — он отвечает, корутина ли перед нами, — и флажок внутри тела корутины, который покажет, выполнилось ли тело.
import asyncio
import inspect
started = False
async def fetch(name):
global started
started = True
return f"{name}: готово"
coro = fetch("Каталог") # вызов БЕЗ await
print("Это корутина:", inspect.iscoroutine(coro))
print("Тело выполнилось:", started)
coro.close() # аккуратно закрыли объект без запуска
Это корутина: True Тело выполнилось: False
Вызов состоялся, а started так и остался False: тело корутины не выполнилось ни на строчку. Вызов async-функции не запускает вычисление — он создаёт и возвращает объект корутины: заготовку выполнения, кадр с кодом и аргументами, который ещё никто не раскрутил. Удобная аналогия: fetch("Каталог") — это рецепт, переписанный на бумажку, а await — момент, когда кто-то наконец идёт на кухню. Пока бумажка просто лежит, еды нет.
Такая ошибка часто появляется не при написании, а при переделке: обычную функцию переписали в async def, а места вызовов остались старыми. Код читается как читался, запуск проходит без ошибок — а действия больше нет. Антидот простой: превратив функцию в корутину, сразу пройдись по всем её вызовам и допиши await — или доверь проверку тесту на реальный результат, а не на отсутствие ошибки.
# print(fetch("Каталог"))
# <coroutine object fetch at 0x7f8b2c3d4e50>
# в repr только тип и адрес памяти - результата внутри нет
await: дождаться и продолжить
await делает ровно две вещи, и обе важны. Первая — запускает корутину и дожидается её до конца: следующая строка не начнётся, пока тело не доработает и не вернёт результат. Вторая — в точке ожидания (например, внутри await asyncio.sleep) корутина уступает управление, и цикл может занять паузу кем-то другим. Пока соседей у нас нет, поэтому на таймстампах await выглядит как обычная очередь: строки выполняются строго по порядку.
import asyncio
import time
START = time.monotonic()
async def fetch(name, delay):
await asyncio.sleep(delay) # имитация сетевой задержки
t = round(time.monotonic() - START)
return f"{name}: готово на t={t}"
async def main():
print(await fetch("Главная", 1))
print(await fetch("Каталог", 2))
asyncio.run(main())
Главная: готово на t=1 Каталог: готово на t=3
«Главная» доехала через секунду, «Каталог» — ещё через две, суммарно t=3: await дождался первой корутины до конца и только потом запустил вторую. Запомни эту цифру: три секунды за две загрузки. Именно этот столбик из await и станет героем урока 5 — там мы посчитаем, почему он не конкурентность, а урок 6 покажет, как те же загрузки ужать до двух секунд.
Второе свойство await стоит подчеркнуть: это полноценное выражение, и его результат можно использовать прямо в месте ожидания — складывать, печатать, передавать в функции, не заводя лишних переменных.
import asyncio
async def fetch(name):
await asyncio.sleep(0) # минимальная пауза: уступили управление
return f"{name}: готово"
async def main():
greeting = "Ответ: " + (await fetch("Курсы"))
print(greeting)
print("Длина отчёта:", len(await fetch("Отчёт")))
asyncio.run(main())
Ответ: Курсы: готово Длина отчёта: 13
asyncio.sleep(0) — честный способ сделать паузу нулевой длины: корутина на миг уступает управление и тут же продолжается. Для песочницы это удобная замена сети там, где важна только механика await, а не время. Длина строки "Отчёт: готово" — 13 символов: await вернул обычную строку, и дальше с ней работает обычный Python.
Где живёт await, а где нет
У await жёсткая прописка: он разрешён только внутри async def. На верхнем уровне обычного скрипта — например, сразу после объявления функции — Python не запустит файл вовсе и сообщит об ошибке ещё до выполнения первой строки:
async def fetch():
return "готово"
result = await fetch()
result = await fetch()
^^^^^^^^^^^^^^
SyntaxError: 'await' outside functionПоэтому и появился в нашем обиходе шаблон async def main() + asyncio.run(main()): внутри main await'ы законны, а asyncio.run — единственная точка, где асинхронный мир стыкуется с обычным. Что именно делает asyncio.run — тема следующего урока.
И ещё одна свобода, о которой стоит знать: asyncio.run принимает любую корутину, не обязательно main. Одноразовую операцию можно запустить и напрямую, без обёртки:
import asyncio
async def fetch(name):
await asyncio.sleep(0)
return f"{name}: готово"
print(asyncio.run(fetch("Курсы")))
Курсы: готово
Для скрипта из одной операции этого достаточно. Как только операций становится несколько, все они переезжают внутрь одной main — и там await выстраивает их по порядку.
async def fetch(url): # 1. async def - объявили корутину
await asyncio.sleep(1) # 2. await - точка паузы и уступки
return f"{url}: готово" # 3. return - результат для await
# result = await fetch("/page") # 4. вызов всегда с await
| Запись | Что происходит |
|---|---|
| async def fetch(): ... | объявили корутину — функцию с правом на паузы; тело пока не выполнялось |
| fetch("Каталог") | вызов создал объект корутины: рецепт без повара; тело всё ещё молчит |
| await fetch("Каталог") | тело выполнено до конца, await вернул результат выражения |
| asyncio.run(main()) | точка входа: раскрутила main, внутри которой await'ы законны |
Что дальше
Оба ключевых слова на месте: async def объявляет корутину, вызов создаёт её объект, await запускает тело и возвращает результат, а жить await может только внутри async def. В следующем уроке спускаемся под капот: что asyncio.run делает с циклом, почему точка входа одна и что случится при попытке запустить цикл изнутри цикла. А если await потерялся в настоящем проекте — урок 4 расскажет, как Python об этом предупреждает и как ловить такие места быстро.
Вызов async-функции не выполняет её тело: он только создаёт объект корутины — рецепт, который никто не начал готовить.
Сначала предскажи ответ в голове — это главный навык программиста.
import asyncio
async def double(n):
await asyncio.sleep(0)
return n * 2
async def main():
x = await double(21)
print(x + await double(9))
asyncio.run(main())
import asyncio
started = False
async def work():
global started
started = True
async def main():
task = work()
print("Тело выполнилось:", started)
asyncio.run(main())
import asyncio
async def word():
await asyncio.sleep(0)
return "кот"
async def main():
a = await word()
print(len(a), a.upper())
asyncio.run(main())
1. Что происходит при вызове fetch("Каталог"), если fetch объявлена через async def?
2. Что делает await fetch(...)?
3. Где синтаксически разрешён await?
4. Что вернёт inspect.iscoroutine(x), если x = fetch("Каталог") без await?
5. Два await подряд: print(await fetch("A", 1)) и print(await fetch("B", 2)). Когда выполнится вторая загрузка?
6. Что произойдёт при запуске скрипта, где объявлена async def main(), но вызова asyncio.run нет?
Напиши корутину shout(word), которая делает минимальную паузу (await asyncio.sleep(0)) и возвращает слово в верхнем регистре. В main получи через await крик для слов «кот» и «пёс» и напечатай их одной строкой через пробел.
Что делает async def в Python?
Объявляет корутину — функцию, которая умеет приостанавливаться в точках await и отдавать управление событийному циклу. Важно: сам вызов такой функции тело не выполняет, он лишь создаёт объект корутины. Чтобы тело отработало, корутину нужно запустить — await'ом внутри другой корутины или asyncio.run снаружи.
Почему моя async-функция не выполняется?
Почти всегда — потерян await. Вызов fetch(...) возвращает объект корутины, тело которого никто не запускал: программа завершается без ошибок и без результата, а в терминале мелькает предупреждение «coroutine ... was never awaited». Замени вызов на await fetch(...) — тело выполнится и вернёт результат.
Можно ли писать await вне async-функции?
Нет: await разрешён только внутри async def. На верхнем уровне обычного скрипта await даёт SyntaxError: 'await' outside function ещё до выполнения. Правильная точка входа — async def main() и запуск asyncio.run(main()).
Как проверить, корутина передо мной или уже результат?
Стандартной проверкой: import inspect, затем inspect.iscoroutine(x). Для не запущенного объекта корутины она вернёт True, для результата await — False. Это удобный приём при отладке: увидел True у переменной, которая должна быть строкой, — где-то выше потерялся await.
Понравился урок? Сошлитесь на него
«Вызов async-функции не выполняет её тело: он только создаёт объект корутины — рецепт, который никто не начал готовить.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
asyncio · Урок 1
Зачем асинхронность: ожидание без блокировки
Три загрузки по секунде — три секунды простоя, если скрипт синхронный. Смотрим на честных замерах, где уходит время, и чем asyncio.sleep отличается от time.sleep.
asyncio · Урок 3
asyncio.run: событийный цикл под капотом
Одна строка asyncio.run(main()) превращает обычный скрипт в асинхронную программу. Разбираем три работы этой строки: создать цикл, раскрутить корутину, убрать за собой.
asyncio · Урок 4
Coroutine never awaited: главная ошибка новичка
Программа завершилась успешно и ничего не сделала: классика потерянного await. Разбираем корутин-призраков, честно смотрим на RuntimeWarning и выучиваем три лекарства.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
aiogram · Урок 7
Асинхронность в aiogram: asyncio для бота без страха
Разбираем на живом коде, зачем aiogram асинхронный: async и await, asyncio.sleep против time.sleep и gather для параллельных чатов.
asyncio для начинающихaiogram async
Matplotlib / Seaborn · Урок 1
Визуализация данных в Python: первое знакомство с Matplotlib
Первый график в Python за три строки кода, анатомия figure и axes, шпаргалка «какой график для каких данных» и сохранение в PNG. Всё запускается прямо на странице.
matplotlib для начинающихчто такое matplotlib
asyncio · Урок 12
Блокирующий вызов останавливает всё
Одна строка time.sleep внутри async-функции — и весь событийный цикл мёртв: никакие задачи не работают, таймеры не тикают. Смотрим на таймстампах и формулируем правило.
asyncio блокировка цикла time.sleeptime.sleep в async функции