День добрый) Ещё при устройстве на работу прикидывал время на привыкание и получалось полгода-год на то, чтобы обвыкнуть на новом месте и появлялись ресурсы думать о чем-то другом в принципе) Примерно так и получилось. Блог на это время выпал из деятельности, зато определился, что хочется и дальше развиваться в этой сфере и накопил материала на несколько постов минимум) Скорее всего одним из ближайших постов напишу сравнение рабочего процесса в маркетинге и отделе разработки)
А сейчас уже 4 и заключительная часть перевода статьи-туториала по асинхронному коду в Python.
________________________________________________________________________________________
Программа: асинхронные запросы
Раз уж вы зашли так далеко, настало время самой веселой и влекательной части. В этой секции мы с вами построим веб скрапер areq.py, с использованием очень быстрого асинхронного фреймворка aiohttp (нам понадобится только его клиентская часть). С такими инструментами мы можем построить связь между кластером сайтов и представить её в виде графа, например.
Заметка: вы можете быть удивлены тем, что библиотека requests не совместима с async IO. Суть в том, что она построена на основе urllib3, которая в свою очередь использует модули Python http и socket.
По умолчанию, операции на сокетах - блокирующие. А значит Python не сможет дождаться await requests.get(url), потому что к методу .get() нельзя применить await. В aiohttp же практически все реализовано с помощью корутин.
На верхнем уровне, структура программы будет выглядеть так:
- Читаем последовательность ссылок из локального файла url.txt.
- Отправляем GET запрос и декодируем полученный контент. Если запрос проваливается, игнорируем эту ссылку.
- Ищем в HTML коде, полученном по ссылке, теги с параметром href.
- Пишем результат в foundurls.txt.
- Делаем это асинхронно и параллельно, насколько это возможно. (используем aiohttp для запросов и aiofiles для работы с файлами. Это 2 главных примера работы с вводом-выводом данных, которые хорошо подходят для иллюстрирования сильных сторон асинхронного подхода).
Файл urls.txt не очень большого объема и содержит в основном самые посещаемые сайты:
Вторая ссылка из списка должна вернуть код 404, который так же необходимо обработать нашей программой. Если у вас расширенная версия этой программы, возможно вы столкнетесь с более серьезными проблемами вроде обрыва связи с сервером и цепочки бесконечных перенаправлений.
Все запросы должны быть сделаны в одной сессии, чтобы получить выгоду от внутреннего пула подключений.
Сейчас посмотрим на полный код программы, мы разберем каждую её часть дальше в материале:
Скрипт получился длиннее, чем наши первые программы-игрушки, так что давайте разобьем его на части.
Константа HREF_RE - это регулярное выражение, которое будет извлекать блоки с атрибутом href в HTML:
Корутина fetch_html() - обертка вокруг GET запроса. Она делает запрос, дожидается ответа и декодирует HTML страницу, если статус был положительный или возвращает исключения, в случае не 200 статуса.
Если все прошло по плану, то fetch_html() возвращает HTML код(строку). В этой функции нет обработки исключений. Логика построена таким образом, чтобы вернуть исключение вызывающей стороне и там его и обработать:
Мы используем await с session.request() и resp.test() потому что это корутины. В ином случае, цикл запрос∕ответ будет занимать большую часть времени действия приложения. С помощью корутин мы позволяем другим операциям (парсинг и запись) работать, пока подготавливаются новые обработанные ссылки.
Следующее звено цепочки корутин - parse(), которое ждет от fetch_html() обработанную ссылку и извлекает все элементы с href атрибутом из HTML кода страницы, проверяя их валидность и преобразовывая в абсолютный путь.
Стоит признать, что вторая часть функции parse() - блокирующая, но она содержит быструю проверку регулярного выражения и обеспечивает преобразование в абсолютный путь.
В этом конкретном случае, синхронный код должен быть быстрым и незаметным. Но помните, что любая строка этого кода будет блокировать остальные корутины, если не используется yield, await или return. Если вам потребуется более сложный парсинг, возможно вам захочется запускать его с использованием отдельного процесса.
Следующая корутина write() берет экземпляр файла и одну из ссылок, дожидается отработки команды parse(), возвращающей набор отработанных ссылок, и асинхронно записывает их в файл, используя aiofiles, библиотеку для асинхронной работы с файлами.
И наконец bulk_crawl_and_write() - служит главной входной точкой в цепь корутин. Задачи, созданные для каждого адреса в файле urls.txt и будут выполняться в рамках одной сессии.
Несколько важных пунктов, которые заслуживают упоминания:
- по умолчанию, в ClientSession встроен адаптер с максимальным числом подключений = 100. Отредактировать это можно в ClientSession в asyncio.connector.TCPConnector. Лимиты можно настраивать отдельно для каждого хоста.
- Вы можете настраивать максимальное время ожидания как для сессии в целом, так и для отдельных запросов
- скрипт так же использует асинхронный менеджер контекста. В нашей статье нет отдельной секции для объяснения этого концепта, т.к. общий переход мне показался достаточно простым. Названия методов поменялись с __exit__(), __enter__() на __aexit__(), __aenter__(). Как вы могли заметить, асинхронность может быть использована только внутри корутины, объявленной с async def.
Если вы хотите продолжить исследование, то можете посмотреть туториал на гитхабе.
И вот, итоги исполнения нашего скрипта - получили, распарсили и сохранили результаты с 9 адресов менее чем за секунду:
Не так уж и плохо. В качестве проверки можно посмотреть сколько строк у файла на выходе. В моем случае - 626, но в вашем результат может отличаться.
Асинхронный код в общем контексте.
Теперь, когда вы увидели живительную дозу кода. Вернемся к основному обсуждению и проверим, когда асинхронный код является идеальным решением и как вы можете оценить, какую из моделей согласованности вам выбрать.
Когда и почему выгодно использовать асинхронный код.
Это не продолжение рассуждений о разнице между асинхронностью, тредами и мультипроцессингом. Однако полезно иметь представление, когда асинхронность будет лучшим решением из перечисленных.
На самом деле, битвы асинхронности и мультипроцессности нет. Они вполне могут сосуществовать в согласии. Если у вас есть множество задач, требующих большое количество ресурсов CPU, то мультипроцессинг - очевидный выбор.
Просто использовать async перед каждой функцией - не лучшая идея, если все эти функции используют блокирующие вызовы. (Это может только еще больше замедлить ваш код) Но, как было разобрано выше, есть определенные ситуации, в которых асинхронность и мультипроцессность может сосуществовать в гармонии. (ссылка на выступлении с конференции на англ. языке)
Противостояние между асинхронностью и тредингом чуть более прямолинейно. Во вступлении я упоминал, что трединг очень сложен. Суть в том, что даже если он выглядит просто, то уследить за всеми состояниями, памятью, сложно отлавливаемыми багами и прочими вещами - сложно.
Так же трединг гораздо менее элегантно масштабируется, потому что ресурсы системы на запуск новых тредов достаточно быстро кончаются. Попытка создать тысячу отдельных тредов на вашей машине скорее всего провалится, я даже не рекомендую пробовать. Создать тысячу асинхронных корутин вполне реально.
Асинхронный код начинает просто сиять, когда у вас есть большое количество задач ввода-вывода, которые в другом случае просто бы блокировали основное выполнение скрипта, например:
- Работа с сетью и клиент-серверными приложениями.
- Безсерверный дизайн, одноранговые многопользовательские сети (вроде групповых чатов).
- Операции чтения∕записи, в которых вы хотите использовать "выстрелил и забыл" стиль, но боитесь забыть блокировку том, что вы читаете или куда пишете данные.
Одна из главных причин не использовать await в том, что далеко не все библиотеки и системы подключения к базам данных поддерживают этот функционал. Если вы хотите добавить асинхронное чтение для какой-то системы управления базой данных, то нужно найти не просто Python обертку, а еще и именно ту, которая поддерживает асинхронные операции. Корутины, которые содержат синхронные вызовы, блокируют другие корутины и операции.
Небольшой список библиотек, поддерживающих async∕await будет в конце этой статьи.
Асинхронный подход, какую библиотеку взять?
Наш материал сфокусирован на асинхронном коде с синтаксисом, содержащим async∕await конструкции и использующем asyncio для управления общим пулом корутин. Но asyncio это не единственная библиотека. Лучше скажет цитата Натаниэля Дж. Смита:
В ближайшие несколько лет, asyncio может оказаться одной из библиотек, избегаемых разработчиками, вроде urllib2.
...
Asyncio стала жертвой своего успеха. Во время её разработки использовались лучшие подходы, но с тех пор появились проекты вдохновленные asyncio. Добавление async∕await изменило ситуацию и мы поняли, что можем сделать лучше. Но asyncio оказалась сдерживаема собственной архитектурой. (источник)
Назовем пару альтернатив, которые используют другое АПИ и другие подходы - curio и trio. Однако на мой взгляд, если вы делаете среднего размера простую программу, проще будет использовать asincio и не добавлять огромную зависимость в свой проект.
В любом случае, посмотрите эти библиотеки. Так вы сможете оценить разные способы решения одних и тех же задач и выбрать наиболее подходящий вам. Однако многие из концептов, которые мы рассмотрели в предыдущих частях не зависят от реализации и должны распространяться и на эти библиотеки.
Полезные мелочи.
В следующих нескольких секциях я раскрою некоторые части asyncio и async∕await, не затронутые ранее, но важные для понимания построения программ.
Другие асинхронные функции высшего уровня.
В дополнение к asyncio.run() вы уже видели функции вроде syncio.create_task() и asyncio.gather().
Вы можете использовать сreate_task(), чтобы запланировать выполнение корутин для asycnio.run():
В примере есть тонкость. Если мы не будет использовать аwait с функцией t в функции main(), то она может отработать до того, как сам main() сообщит о завершении t. Это происходит потому что asyncio.run(main()) вызывает цикл до завершения функции main(). Подразумевается, что если main() отработала, то все корутины внутри неё так же отработали и их можно завершить. Чтобы получить текущие задачи, вы можете использовать asyncio.Task.all_tasks()
Заметка: asyncio.create_task() появилось только в Python 3.7. В Python 3.6 или ниже используйте asyncio.ensure_future() вместо create_task().
Отдельно есть asyncio.gather(). Эта команда позволяет аккуратно разместить корутины в одном future-объекте. Как результат, она возвращает future-объект, в котором вы можете указать, какие именно корутины хотите дождаться. В чем то это похоже на работу с очередями и queue.join() из ранних примеров. Результатом gather() будет список результатов корутин:
Вы скорее всего заметили, что gather() ждет общего результата всех корутин, которые вы запустили. Другой способ - проитерироваться через asyncio.as_completed() чтобы получить задачи, которые уже завершены в порядке завершения. Эта функция вернет генератор, возвращающий завершившиеся задачи. Далее вы увидите результат coro([3, 2, 1]), который будет будет доступен раньше coro([10, 5, 0]), а не одновременно, как в случае с gather():
Приоритет ожидания.
Несмотря на похожесть у команды await более высокий приоритет чем у yield. Поэтому в некоторых случаях вам могут понадобиться скобки. Для более подробной информации смотрите примеры из PEP 492.
Заключение.
Теперь вы достаточно подкованы, чтобы использовать async∕await и библиотеки построенные на них. Краткий перечень освещенных тем:
- Асинхронный подход - не зависящая от языка модель, и способ наладить одновременное исполнение программ, с помощью запуска корутин, косвенно взаимодействующих с другим.
- Специфичные для Python инструменты - async∕await. Используйте их для определения корутин
- asyncio - Python библиотека, которая предоставляет АПИ для запуска и управления корутинами.
__________________________________________________________________________________________
Ееееи, мой долг по переводам наконец-то доделан)
В следующих постах немного о базе в работе с linux, памятка по регулярным выражениям и командной строке и общее впечатление от работы в отделе разработки.
Список библиотек, о котором говорилось в материале.
Libraries That Work With async/await
From aio-libs:
- aiohttp: Asynchronous HTTP client/server framework
- aioredis: Async IO Redis support
- aiopg: Async IO PostgreSQL support
- aiomcache: Async IO memcached client
- aiokafka: Async IO Kafka client
- aiozmq: Async IO ZeroMQ support
- aiojobs: Jobs scheduler for managing background tasks
- async_lru: Simple LRU cache for async IO
From magicstack:
From other hosts:
- trio: Friendlier asyncio intended to showcase a radically simpler design
- aiofiles: Async file IO
- asks: Async requests-like http library
- asyncio-redis: Async IO Redis support
- aioprocessing: Integrates multiprocessing module with asyncio
- umongo: Async IO MongoDB client
- unsync: Unsynchronize asyncio
- aiostream: Like itertools, but async