Представьте официанта в кафе. Он принял заказ за первым столиком и передал его на кухню. Пока блюдо готовится, официант не стоит у плиты и не ждёт — он идёт принимать заказ за вторым столиком, потом за третьим, разносит напитки, убирает посуду. Когда блюдо готово — кухня зовёт его, и он его забирает. Один официант обслуживает сразу много столиков, потому что не тратит время на ожидание, а переключается между делами.
Именно так работает асинхронный код в Python. Раньше в курсе вы писали программы, где каждая строка выполняется только после того, как закончилась предыдущая. Если строка «ждёт» — например, ответа от сервера в интернете — вся программа стоит и ничего больше не делает. Для одной задачи это нормально. Но если задач много и каждая содержит ожидание, время складывается, и программа работает медленно без всякой причины: процессор всё это время простаивает, а не считает.
В этом уроке разберём инструменты, которые позволяют не ждать впустую — async, await, модуль asyncio — а затем применим их на практике с библиотекой aiohttp, которая умеет забирать данные сразу из нескольких источников в интернете, а не по одному.
Синхронный и асинхронный код — в чём разница
Весь код, который вы писали до сих пор в этом курсе, — синхронный: строки выполняются строго по очереди, и следующая строка не начнётся, пока не закончится предыдущая. Если где-то есть ожидание (ответа сервера, файла, таймера) — программа замирает на этом месте.
Асинхронный код устроен иначе: когда одна задача говорит «я жду», программа не замирает, а переключается на другую задачу, которая тоже готова работать. Как только ожидание первой задачи закончилось — программа вернётся к ней. Всё это происходит в одном потоке, без создания параллельных процессов: просто умное чередование задач в моменты ожидания.
| Задачи по очереди (синхронно) | Задачи с переключением (асинхронно) |
|---|---|
| Ждём ответа от сайта №1 (2 сек), потом ждём ответа от сайта №2 (2 сек). Итого 4 секунды. | Отправили запросы к обоим сайтам сразу, ждём вместе. Итого около 2 секунд. |
Асинхронность помогает именно там, где программа ждёт: ответа сети, чтения файла с диска, ответа базы данных. Такие задачи называют I/O-bound (от input/output — ввод-вывод). Если же программа не ждёт, а постоянно считает — например, перемножает большие числа — асинхронность ничем не поможет: переключаться не на что, процессор и так занят все время.
Event loop — кто переключает задачи
У официанта в аналогии есть план в голове: кого обслужить следующим. В Python эту роль играет объект, который называется event loop («цикл событий»). Это диспетчер, который следит за всеми задачами программы и решает, какая из них готова поработать прямо сейчас.
Event loop работает по кругу: смотрит на список задач, запускает готовую, и как только она сообщает «я жду» — забирает у неё управление и передаёт следующей готовой задаче. Когда ожидание какой-то задачи заканчивается, event loop возвращается и продолжает её с того же места.
Важно понимать: переключение между задачами происходит только в те моменты, когда задача сама говорит «я жду». Если задача ничего не ждёт и просто считает — она займёт event loop целиком, и остальные задачи не смогут поработать, пока она не закончит.
async def — функция, которую можно приостановить
Обычная функция, объявленная через def, начинает работать сразу при вызове и не может остановиться на середине. Чтобы функция умела «ждать» и в этот момент отдавать управление event loop, её объявляют через async def. Такая функция называется корутиной (coroutine, «сопрограмма»).
async def say_hello():
print("Привет!")
result = say_hello()
print(type(result))
Разберём построчно:
async def say_hello():— объявляет корутину с именемsay_hello. Словоasyncпередdef— единственное отличие от обычной функции в объявлении.result = say_hello()— вызов корутины, как обычной функции. Но код внутри неё не выполняется: вызов только создаёт специальный объект-корутину, который ещё предстоит запустить.print(type(result))— выведет<class 'coroutine'>. Слово «Привет!» так и не напечаталось.
Это самое частое удивление новичков: вызов корутины сам по себе ничего не запускает. Чтобы код внутри неё реально выполнился, нужен await или asyncio.run() — о них дальше.
await — дождаться результата, не блокируя остальных
Слово await ставится перед вызовом корутины и означает: «запусти эту корутину, дождись её результата, а пока ждёшь — отдай управление event loop, пусть поработает кто-то другой».
import asyncio
async def greet(name):
print(f"Привет, {name}!")
await asyncio.sleep(1)
print(f"Пока, {name}!")
import asyncio— стандартный модуль Python для асинхронного кода, ничего устанавливать не нужно.async def greet(name):— корутина с одним параметромname, объявляется как обычная функция, только сasync.print(f"Привет, {name}!")— обычная строка, выполняется сразу, ничего асинхронного тут нет.await asyncio.sleep(1)—asyncio.sleep(1)это тоже корутина, которая «спит» секунду.awaitперед ней запускает эту паузу и, пока она идёт, отдаёт event loop другим задачам — сама функцияgreetв это время приостановлена на этой строке.print(f"Пока, {name}!")— выполнится только после того, как секунда прошла иawaitвернул управление обратно вgreet.
await работает только внутри async def
Если написать await в обычной функции без async перед def — Python выдаст SyntaxError. Логично: «отдать управление event loop» может только код, который сам умеет приостанавливаться, а это умеют только корутины.
asyncio.run() — вход в асинхронный мир
Весь код программы, который выполняется, когда вы просто запускаете файл, — синхронный: у него ещё нет event loop, который бы управлял корутинами. Значит, откуда-то нужно этот event loop создать и запустить в нём первую корутину. Для этого есть функция asyncio.run().
import asyncio
async def main():
print("Начало")
await asyncio.sleep(2)
print("Конец (прошло 2 секунды)")
asyncio.run(main())
async def main():— по договорённости корутину верхнего уровня часто называютmain, это просто имя, не ключевое слово.asyncio.run(main())— создаёт новый event loop, запускает в нём корутинуmain(), дожидается её полного завершения и закрывает event loop. Это единственная строка во всём файле, которая не находится внутриasync def.
asyncio.run() вызывают один раз за программу, обычно последней строкой файла. Всё остальное асинхронное происходит уже внутри, через await.
Ловушка: asyncio.sleep() и time.sleep() — не одно и то же
Вы уже могли встречать модуль time и его функцию time.sleep(секунды) — она тоже делает паузу. Но внутри корутины использовать её нельзя.
import time
async def bad():
print("Жду...")
time.sleep(2)
print("Дождался")
time.sleep(2) — это обычная функция, не корутина, перед ней не ставят await. Она буквально останавливает весь поток на 2 секунды, и event loop не может в это время переключиться ни на одну другую задачу — программа зависает целиком, хотя формально написана как асинхронная.
asyncio.sleep(2), в отличие от неё, — корутина. Вызванная через await, она приостанавливает именно ту функцию, где стоит, и отдаёт event loop свободное время для других задач.
Правило: внутри async def для пауз и ожиданий используйте только асинхронные версии — await asyncio.sleep(...). Обычный time.sleep() ломает саму идею асинхронности.
asyncio.gather() — запустить несколько корутин одновременно
Пока мы делали только одно ожидание за раз. Настоящая польза асинхронности видна, когда независимых ожиданий несколько. asyncio.gather() запускает переданные ей корутины все сразу и возвращает список их результатов, когда завершится последняя.
import asyncio
async def fetch(name, delay):
await asyncio.sleep(delay)
return f"данные от {name}"
async def main():
results = await asyncio.gather(
fetch("API 1", 2),
fetch("API 2", 3),
fetch("API 3", 1),
)
print(results)
asyncio.run(main())
fetch(name, delay)— корутина, которая ждётdelayсекунд и возвращает строку с именем. Имитирует запрос, который отвечает не мгновенно.asyncio.gather(fetch("API 1", 2), fetch("API 2", 3), fetch("API 3", 1))— каждый вызовfetch(...)создаёт корутину, все три передаются вgatherкак отдельные аргументы.gatherзапускает их одновременно.awaitпередgather— ждём, пока завершится самая долгая из трёх корутин: 3 секунды, а не 2 + 3 + 1 = 6 секунд, как было бы при обычном вызове одной за другой.results— список из трёх строк, в том же порядке, что и в вызовеgather, независимо от того, какая корутина закончилась раньше остальных.
Именно в этом суть асинхронности: не по одному запросу с ожиданием, а сразу пачка запросов, которые ждут вместе.
aiohttp — зачем нужна асинхронность на практике
Самое частое реальное применение асинхронности — запросы в интернет: к сайтам, к API, к внешним сервисам. Каждый такой запрос — классическая I/O-bound операция: программа отправляет вопрос и ждёт ответа сервера, при этом процессор простаивает.
Библиотека aiohttp — асинхронная версия работы с HTTP. Она не входит в стандартную библиотеку Python, её устанавливают отдельно (pip install aiohttp, вы уже видели pip раньше в курсе). Смысл aiohttp — если нужно получить данные с нескольких адресов, она отправляет запросы все сразу, а не один за другим, и суммарное время ожидания получается не суммой, а максимумом — ровно как в примере с gather выше.
Код с aiohttp в этом разделе — для чтения и понимания идеи. В песочнице курса нет доступа в интернет, поэтому в задачах урока используются только asyncio.sleep() для имитации ожидания — сама логика ожидания и переключения работает точно так же.
ClientSession и запрос GET
Все запросы в aiohttp делаются через объект ClientSession — он хранит настройки соединения и переиспользует их для всех запросов внутри него, вместо того чтобы каждый раз соединяться заново. Открывают сессию через async with — конструкцию, которую вы уже разбирали для файлов; она сама закроет соединение, когда работа завершится.
import aiohttp
import asyncio
async def main():
url = "https://jsonplaceholder.typicode.com/posts/1"
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
print(response.status)
data = await response.json()
print(data["title"])
asyncio.run(main())
async with aiohttp.ClientSession() as session:— открывает сессию с именемsession, автоматически закроет её на выходе из блока.async with session.get(url) as response:— отправляет GET-запрос («дай мне данные») по адресуurlи открывает ответ какresponse, тоже черезasync with, потому что получение ответа — тоже операция ожидания.response.status— код ответа сервера:200означает «всё хорошо»,404— «не найдено».await response.json()— забирает тело ответа и сразу превращает его из JSON-текста в обычный Python-словарь. Раз это операция ожидания (данные ещё нужно получить и разобрать) — перед ней тожеawait.
Несколько запросов одновременно
Вот ради чего всё это затевалось. Соединим ClientSession и уже знакомый asyncio.gather(), чтобы забрать данные сразу с нескольких адресов:
import aiohttp
import asyncio
async def fetch_user(session, user_id):
url = f"https://jsonplaceholder.typicode.com/users/{user_id}"
async with session.get(url) as response:
data = await response.json()
return data["name"]
async def main():
async with aiohttp.ClientSession() as session:
tasks = [fetch_user(session, i) for i in range(1, 4)]
names = await asyncio.gather(*tasks)
print(names)
asyncio.run(main())
fetch_user(session, user_id)— отдельная корутина на один запрос; сессию передаём как параметр, чтобы не открывать новую на каждый запрос.tasks = [fetch_user(session, i) for i in range(1, 4)]— список генератором создаёт три корутины (для id 1, 2 и 3), но пока ни одна из них не запущена.await asyncio.gather(*tasks)— звёздочка*распаковывает список в три отдельных аргумента, как и при вызове функции с несколькими параметрами.gatherзапускает все три запроса сразу и ждёт их вместе.names— список из трёх имён, в том же порядке, в котором стояли задачи вtasks.
Частая ошибка: открывать новую ClientSession() на каждый запрос внутри цикла. Сессия должна быть одна на всю группу запросов — создавайте её один раз и передавайте туда, где она нужна.
Частые ошибки
Забыли await перед корутиной
async def main():
greet("Аня") # без await — код внутри greet не выполнится
asyncio.run(main())
Python не выбросит ошибку сразу, но напечатает предупреждение RuntimeWarning: coroutine 'greet' was never awaited, а сам код функции так и не запустится. Правило простое: любой вызов корутины требует либо await перед ним, либо передачи в asyncio.run() или asyncio.gather().
await вне async def
def main(): # обычная функция, без async
await asyncio.sleep(1) # SyntaxError
await можно писать только внутри функции, объявленной через async def. Если убрали async по невнимательности — получите ошибку синтаксиса ещё до запуска.
time.sleep() вместо asyncio.sleep()
Разобрали это подробно выше: time.sleep() внутри корутины замораживает всю программу целиком, а не только текущую задачу. Если в асинхронном коде нужна пауза — только await asyncio.sleep(...).
Куда двигаться дальше
Асинхронность и aiohttp — это именно то, на чём строятся телеграм-боты: бот одновременно отвечает десяткам пользователей, потому что не ждёт ответа одного, пока не разберётся с другим. Если хотите написать своего бота с нуля — на платформе есть отдельный курс «Телеграм-боты на aiogram», который начинается ровно с того места, где заканчивается этот урок.
Что важно запомнить
- Асинхронность — это переключение между задачами в одном потоке в моменты ожидания, а не настоящая параллельность.
- Помогает там, где программа ждёт сеть, диск или базу данных (I/O-bound); для чистых вычислений (CPU-bound) не даёт выигрыша.
- event loop — диспетчер, который решает, какая задача работает прямо сейчас.
async defобъявляет корутину; вызов безawaitне запускает код внутри, а только создаёт объект-корутину.awaitзапускает корутину, ждёт её результат и в момент ожидания отдаёт управление event loop; работает только внутриasync def.asyncio.run(...)— точка входа в асинхронный код из обычного, синхронного места программы.await asyncio.sleep(...)— неблокирующая пауза;time.sleep(...)внутри корутины замораживает всю программу.await asyncio.gather(корутина1, корутина2, ...)запускает несколько корутин одновременно и возвращает список их результатов по порядку.aiohttp.ClientSession()— одна сессия на все запросы;await response.json()сразу превращает ответ сервера в Python-словарь.
Проверьте себя
8 вопросов
Асинхронный таймер
Напиши асинхронную функцию countdown(name, seconds), которая:
- Печатает
"{name}: старт!" - Каждую секунду печатает
"{name}: {осталось} сек" - В конце печатает
"{name}: финиш!"
Используй asyncio.sleep(1) для паузы, а не time.sleep().