Перейти к содержанию

192 уроков, 14 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 2 из 20 Начальный 30 мин 110 XP

Первая корутина: 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 делает ровно две вещи, и обе важны. Первая — запускает корутину и дожидается её до конца: следующая строка не начнётся, пока тело не доработает и не вернёт результат. Вторая — в точке ожидания (например, внутри await asyncio.sleep) корутина уступает управление, и цикл может занять паузу кем-то другим. Пока соседей у нас нет, поэтому на таймстампах await выглядит как обычная очередь: строки выполняются строго по порядку.

два 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 стоит подчеркнуть: это полноценное выражение, и его результат можно использовать прямо в месте ожидания — складывать, печатать, передавать в функции, не заводя лишних переменных.

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 не запустит файл вовсе и сообщит об ошибке ещё до выполнения первой строки:

await вне async-функции не живёт
async def fetch():
    return "готово"

result = await fetch()
Вывод
    result = await fetch()
             ^^^^^^^^^^^^^^
SyntaxError: 'await' outside function
Блок не запускается намеренно: он вызывает ошибку. Файл даже не начинает выполняться — SyntaxError ловится на стадии разбора.

Поэтому и появился в нашем обиходе шаблон async def main() + asyncio.run(main()): внутри main await'ы законны, а asyncio.run — единственная точка, где асинхронный мир стыкуется с обычным. Что именно делает asyncio.run — тема следующего урока.

И ещё одна свобода, о которой стоит знать: asyncio.run принимает любую корутину, не обязательно main. Одноразовую операцию можно запустить и напрямую, без обёртки:

asyncio.run принимает любую корутину
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
Каркас для чтения: импорты и запуск через asyncio.run опущены. Четыре пронумерованных строки — весь словарь урока.
ЗаписьЧто происходит
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())
Проверь себя
0 / 6

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 нет?

Карточки терминов
Запомнено: 0 / 6
Практика

Напиши корутину shout(word), которая делает минимальную паузу (await asyncio.sleep(0)) и возвращает слово в верхнем регистре. В main получи через await крик для слов «кот» и «пёс» и напечатай их одной строкой через пробел.

practice.py
Вопросы и ответы по уроку

Что делает 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-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.