Awaitable-объекты — это объекты, которые ожидают результатов выполнения других awaitable-объектов. Они могут передавать управление другим awaitable-объектам, пока сами бездействуют. И так по цепочке, пока не будет выполнено всё необходимое.
Например, предположим, вы написали код, который должен выполнить 100 запросов и получить содержимое ответа. В этом коде awaitable-объектом будет служить сам запрос, из которого вы хотите получить текст. Все подобные запросы должны запускаться с оператором await, как показано в коде ниже:
async with aiohttp.ClientSession() as session: async with session.get(url) as response: soup = BeautifulSoup(await response.text(), 'lxml')
Когда awaitable-объект отправляет запрос, он сообщает циклу событий, что готов продолжить свою работу после получения ответа от сервера. Цикл событий в это время имеет информацию обо всех запущенных awaitable-объектах. Он либо выполнит все задачи, либо упадет с ошибкой, если не обработать исключения (об этом мы будем говорить в следующих разделах курса).
3 типа Awaitable объектов
Сопрограммы
Сопрограмма (coroutine) — это асинхронная функция, созданная с использованием ключевого слова async. Результатом выполнения любой сопрограммы является awaitable-объект coroutine, который может быть передан в цикл событий. По этой причине любая сопрограмма также считается awaitable-объектом. Сопрограмма может быть вызвана в теле другой сопрограммы; для этого используется ключевое слово await перед вызовом вложенной сопрограммы.
import asyncio
async def nested():
return print('Сопрограмма вызвана внутри сопрограммы async main()')
async def main():
await nested()
asyncio.run(main())
#Результат
Сопрограмма вызвана внутри сопрограммы async main()
Когда сопрограмма передает управление другой сопрограмме, она приостанавливается, сохраняя все свои свойства. Когда управление будет возвращено, она продолжит работу с того места, на котором остановилась. Таким образом, возникает ощущение, что они работают одновременно.
Для чего использовать сопрограммы при написании парсеров?
Мотором любого асинхронного кода является цикл событий, а сопрограммы — это его топливо. Именно в них происходит работа над обработкой и сохранением собранной вами информации.
Любой парсер можно разделить на сопрограммы для выполнения некоторых действий параллельно, например, одновременный сбор данных и запись их в файл.
Основное преимущество сопрограмм — это их эффективность и простота написания. Сопрограмма работает в цикле событий, который самостоятельно управляет ресурсами вашего компьютера. Вы указываете место, где нужно переключать сопрограммы, при помощи ключевого слова await. Дальше цикл событий будет переключаться между сопрограммами и управлять их работой самостоятельно; вам не нужно об этом думать.
Недостатки сопрограмм
Первое и, наверное, основное — то, что многим новичкам очень сложно разобраться в основах асинхронного программирования. По этой причине асинхронность изучают либо специалисты, которые разбираются в Python на должном уровне, либо для работы с узкой специализацией, например, для асинхронного парсинга.
Сопрограммы — это высокоуровневое решение. Ускорить сложные вычисления с их помощью не получится; в этом случае нужна многопоточность или многопроцессорность. Но для написания парсеров нам это и не нужно.
Task — задачи
Задачи в asyncio.task используются для одновременного планирования запуска нескольких сопрограмм. В следующих разделах курса вы увидите, что мы создаем отдельный task для каждого запроса. Для оборачивания задачи в task используется функция asyncio.create_task(). Когда задача обернута, она автоматически запустится, когда подойдет ее очередь.
import asyncio
async def nested(text, number):
return print(text, number)
async def main():
task = asyncio.create_task(nested('Переданное число', 333))
await task
asyncio.run(main())
#Результат
Переданное число 333
При планировании task мы можем передавать необходимые аргументы в сопрограммы. В коде выше мы передали две переменные — text и number. После оборачивания функции с аргументами в task мы использовали ключевое слово await для указания места переключения работы сопрограммы. Конкретно в этом примере сопрограмма выполняет только одну задачу — печатает один раз переданные аргументы. При написании парсеров мы будем оборачивать каждую task и передавать в аргументах открытую сессию, ссылку и что-нибудь еще, что понадобится для работы другой сопрограммы.
Futures — объекты с будущими результатами
Самая непростая для понимания вещь — это как раз таки объекты-футуры. Мы не создаем их на уровне приложения; они в основном создаются автоматически при вызове асинхронных функций или методов.
Если коротко, то объект future инкапсулирует асинхронное выполнение вызываемого объекта, предоставляя другой низкоуровневый объект, который хранит в себе состояние запущенного awaitable объекта, сопрограммы или task, в том случае, если они чего-то ожидают. Именно благодаря футурам цикл событий знает, какой результат принадлежит какой сопрограмме или task. Объект future хранит в своей памяти информацию о том, что задача еще не выполнена или выполнена не до конца. Он также может хранить в себе полученный результат или исключение, полученное во время выполнения кода. Для написания парсеров мы не будем так глубоко погружаться в изучение асинхронного программирования и работать с футурами; они останутся для нас «под капотом».