Привет, Хабр! Перед вами шестая часть (1, 2, 3, 4, 5) перевода руководства по модулю asyncio в Python. Здесь представлены 14-16 разделы исходного материала. Блокирующая задача — это такая задача, которая останавливает выполнение инструкций текущего потока. Если в asyncio-программе запускается блокирующая задача — она останавливает весь цикл событий, не давая выполняться любым другим корутинам. Блокирующие вызовы в asyncio-программах можно выполнять асинхронно, пользуясь функциями asyncio.to_thread() и loop.run_in_executor(). Суть asyncio — это асинхронное программирование и неблокирующий ввод/вывод. Но, в то же время, в asyncio-приложениях часто нужно выполнять вызовы блокирующих функций.Такая необходимость может возникать по множеству причин. Вот некоторые из них: Для решения задачи, зависящей от производительности CPU, например — выполняющей какие-то вычисления. Для выполнения блокирующей операции, зависящей от подсистемы ввода/вывода. Это может быть чтение или запись файла. Для вызова функции из сторонней библиотеки, которая ещё не поддерживает механизмы модуля asyncio. Непосредственное выполнение блокирующего вызова в asyncio-программе приведёт к остановке цикла событий на время работы этого вызова. Это не позволит другим корутинам выполняться в фоновом режиме. Как выполнить блокирующий вызов в asyncio-программе асинхронно? Модуль asyncio даёт нам два инструмента для выполнения блокирующих вызовов в asyncio-программах. Первый — это функция asyncio.to_thread(). Второй — функция loop.run_in_executor(). Функция asyncio.to_thread() — это высокоуровневый API, который рассчитан на разработчиков приложений. Эта функция принимает имя функции, которую нужно вызвать в асинхронном режиме, а так же — аргументы, необходимые переданной ей функции. Функция, переданная asyncio.to_thread(), выполняется в отдельном потоке. Функция asyncio.to_thread() возвращает корутину, завершения работы которой можно подождать, выполнение которой можно запланировать в виде независимой задачи. Например: Задача, переданная asyncio.to_thread(), не начнёт выполняться до тех пор, пока возвращённой корутине не будет предоставлена возможность выполниться в цикле событий. Внутри функции asyncio.to_thread() создаётся объект ThreadPoolExecutor для выполнения блокирующих вызовов. В результате получается, что эта функция пригодна лишь для выполнения задач, зависящих от подсистемы ввода/вывода. Как уже было сказано, второй подход к выполнению блокирующих вызовов в asyncio-программах заключается в применении функции loop.run_in_executor(). Эта функция представляет низкоуровневый API asyncio, перед её применением нужно получить доступ к циклу событий. Например — с помощью функции asyncio.get_running_loop(). Функция loop.run_in_executor() принимает исполнитель и функцию, которую нужно выполнить. Если в качестве исполнителя ей предоставлено значение None — используется исполнитель, применяемый по умолчанию — ThreadPoolExecutor. Функция loop.run_in_executor() возвращает объект, допускающий ожидание, завершения работы которого, при необходимости, можно подождать. Задача начнёт выполняться немедленно. Поэтому для того чтобы блокирующая задача начала бы выполняться, нет необходимости дожидаться завершения работы возвращённого объекта, допускающего ожидание, не нужно и планировать его выполнение. Например: Как вариант, исполнитель может быть создан и передан функции loop.run_in_executor(). Она будет выполнять в этом исполнителе асинхронные вызовы. Вызывающая сторона в таком случае отвечает за управление исполнителем, за его закрытие после завершения работы с ним. Например: Эти два подхода позволяют выполнять в asyncio-программах блокирующие вызовы в виде асинхронных задач. Теперь, когда мы знаем о том, как выполнять блокирующие вызовы, рассмотрим пример. Исследуем выполнение блокирующих операций ввода/вывода в asyncio-программах с применением функции asyncio.to_thread(). В этом примере мы определим функцию, которая на несколько секунд блокирует вызывающую сторону. Затем мы асинхронно выполним эту функцию в пуле потоков asyncio, пользуясь функцией asyncio.to_thread(). Это освободит вызывающую сторону, даст ей возможность выполнять корутины, решающие другие задачи. Вот код примера: При запуске этого кода сначала создаётся корутина main(), которая играет роль точки входа в программу. Эта корутина запускается и выводит сообщение. После этого она выполняет вызов блокирующей функции, выполнять которую планируется с помощью пула потоков. В результате этой операции возвращается корутина. Затем возвращённую корутину оборачивают в объект Task и выполняют независимо от остального кода. Теперь корутина main() может продолжать выполнение других задач. В данном случае она ненадолго «засыпает», позволяя запланированной задаче начать выполняться. Благодаря этому целевая функция передаётся, в недрах системы, исполнителю ThreadPoolExecutor и начинает выполняться. Далее — корутина main() приостанавливается и ждёт завершения работы задачи. Блокирующая функция выводит первое сообщение, «засыпает» на 2 секунды, а после этого выводит последнее сообщение. Этот пример демонстрирует возможность асинхронного выполнения блокирующих операций ввода/вывода с использованием отдельного потока. Подробности о функции asyncio.to_thread() можно найти здесь. Теперь поговорим о том, как разрабатывать и использовать асинхронные итераторы. Итерирование некоего объекта — это одна из базовых операций в Python. Итерировать можно списки, строки, а так же — разные другие объекты. Модуль asyncio позволяет разрабатывать асинхронные итераторы. Создавать и использовать такие итераторы в asyncio-программах можно, определяя объекты, реализующие методы aiter() и anext(). Познакомимся с асинхронными итераторами поближе. Асинхронный итератор — это объект, который реализует методы aiter() и anext(). Прежде чем мы ближе познакомимся с асинхронными итераторами — вспомним о том, что собой представляют итераторы классические. Итератор — это Python-объект, который реализует специфический интерфейс. В частности, речь идёт о методе iter(), который возвращает экземпляр итератора, и о методе next(), который инициирует выполнение одного шага итератора и возвращает значение. Итератор: объект, представляющий поток данных. Повторяющиеся вызовы метода итератора next() (или его передача встроенной функции next()) возвращают последовательные элементы из потока. Когда в итераторе больше не остаётся данных, вместо возврата нового значения возбуждается исключение StopIteration. Python glossary Обходить итератор можно с помощью встроенной в Python функции next() или — воспользовавшись циклом for. Многие Python-объекты являются итерируемыми. В частности — речь идёт об объектах-контейнерах — таких, как списки. Асинхронный итератор — это Python-объект, реализующий специфический интерфейс. Асинхронный итератор: объект, реализующий методы aiter() и anext(). Python glossary Асинхронный итератор обязан реализовать методы aiter() и anext(). Метод aiter() должен возвращать экземпляр итератора. Метод anext() должен возвращать объект, допускающий ожидание, который выполняет шаг итератора. Пройтись по шагам асинхронного итератора, или обойти его, можно только в asyncio-программе. Например — в корутине. Асинхронные итераторы были представлены в PEP 492 – Coroutines with async and await syntax. Обойти асинхронный итератор можно с помощью встроенной функции anext(), которая возвращает объект, допускающий ожидание, выполняющий один шаг итератора — то есть — один вызов метода anext(). Асинхронный итератор можно обойти и используя конструкцию async for, которая автоматически вызывает anext() на каждой итерации и дожидается завершения работы полученного объекта, допускающего ожидание, чтобы получить возвращённое значение. Асинхронный итерируемый объект способен вызывать асинхронный код в своей реализации метода iter, а асинхронный итератор может вызывать асинхронный код в своём методе next. PEP 492 – Coroutines with async and await syntax Цикл async for используется для обхода асинхронных итераторов. Это — асинхронный вариант обычного цикла for. Асинхронный итератор — это итератор, который выдаёт объекты, допускающие ожидание. Напомню, что вызывающий код может ждать результатов работы объекта, допускающего ожидание, такого, как корутина или задача. Объект, допускающий ожидание: объект, который может быть использован в выражении await. Python glossary Асинхронный генератор автоматически реализует методы асинхронного итератора, что позволяет итерировать его так же, как асинхронный итератор. Конструкция async for позволяет вызывающей стороне обходить асинхронный итератор, выдающий объекты, допускающие ожидание, и получать от каждого из них результат его работы. Это — не то же самое, что обход коллекции или списка объектов, допускающих ожидание (например — объектов корутин). Вместо этого объекты, допускающие ожидание, должны быть предоставлены с использованием методов асинхронного итератора, наличие которых ожидается у объекта, с которым ведётся работа. Внутренние механизмы цикла async for автоматически разрешают объекты, допускающие ожидание, или ждут результатов работы таких объектов, при необходимости планируя выполнение корутин. Так как речь идёт о разновидности цикла for — предполагается, хотя и не требуется, чтобы каждый объект, допускающий ожидание, обход которого осуществляется, вырабатывал бы возвращаемые значения. Цикл async for необходимо использовать только внутри корутин, так как в его реализации применяется выражение await, которое можно использовать только внутри корутины. Цикл async for можно использовать в корутинах для обхода асинхронных итераторов. Например: Применение подобной конструкции не приведёт к распараллеливанию операций, выполняемых в цикле for. Модуль asyncio не может выполнять в одном Python-потоке больше одной корутины за раз. Цикл async for правильнее будет воспринимать как асинхронный вариант цикла for. Особенность асинхронного цикла заключается в том, что корутина, которая его выполняет, будет приостанавливаться и ожидать, самостоятельно пользуясь await, результатов работы каждого объекта, допускающего ожидание. На более глубоком уровне это может означать необходимость планирования выполнения корутин и ожидания завершения их работы (это можно сказать и о задачах). Цикл async for, кроме того, можно использовать в роли выражения в механизме спискового включения: Применение подобной конструкции приведёт к созданию списка возвращённых значений, полученных из асинхронного итератора. Теперь поговорим о том, как определять, создавать и использовать асинхронные итераторы. Здесь мы поговорим о том, как определять и создавать асинхронные итераторы, как добиваться от них получения очередного значения и как их обходить. Начнём с определения асинхронного итератора. Асинхронный итератор можно определить, определив класс, который реализует методы aiter() и anext(). Эти методы определяются в объекте так же, как и любые другие методы. Тут важно обратить внимание на то, что, так как функция anext() должна возвращать объект, допускающий ожидание, в её определении необходимо использовать конструкцию async def. object.anext(self): должен возвращать объект, допускающий ожидание, что приводит к выдаче следующего значения итератора. Этот метод должен вызывать ошибку в том случае, если значения в итераторе закончились. Asynchronous iterators Когда итератору нечего больше возвращать — метод anext() должен вызывать исключение StopAsyncIteration. Например: Асинхронный итератор — это корутина. Такие итераторы возвращают объекты, допускающие ожидание, которые планируются на выполнение и выполняются в цикле событий asyncio. Поэтому можно выполнять объекты, допускающие ожидание, и дожидаться их результатов в пределах тела итератора. Например: Теперь поговорим об использовании асинхронных итераторов. Прежде чем воспользоваться асинхронным итератором — надо создать его экземпляр. Это выглядит так же, как и создание обычных Python-объектов: В ходе работы этого кода возвращается «асинхронный итерируемый объект», который является экземпляром «асинхронного итератора». Для того чтобы итератор выполнил бы один шаг и возвратил бы очередное значение — можно воспользоваться встроенной функцией anext(). Выглядит это так же, как применение функции next() при работе с обычным итератором. Результат одного шага итератора получают, дождавшись завершения работы объекта, допускающего ожидание. Например: Эти две операции можно выполнить в одну строку: Асинхронный итератор можно обойти в цикле, воспользовавшись конструкцией async for, которая автоматически организует ожидание получения результатов каждого шага итератора. Например: Подробности о цикле async for можно найти здесь. Цикл async for можно использовать и в асинхронном варианте механизма спискового включения для сбора результатов работы итератора: Исследуем работу с асинхронными итераторами с применением цикла async for. Здесь представлен доработанный вариант примера из предыдущего раздела. Обход итератора в данном случае осуществляется с помощью цикла async for. Этот цикл автоматически дожидается результатов работы каждого объекта, допускающего ожидание, возвращённого из итератора. Далее, он получает возвращаемое значение, которое оказывается доступным в теле цикла, что позволяет его вывести. Это, вероятно, наиболее часто встречающийся паттерн использования асинхронных итераторов. Вот полный код примера: Здесь сначала создаётся корутина main(), используемая как точка входа в asyncio-программу. В ходе работы этой корутины запускается асинхронный цикл for. Далее — создаётся экземпляр асинхронного итератора, после чего цикл автоматически получает его элементы, используя, для возврата объектов, допускающих ожидание, функцию anext(). После получения очередного элемента, цикл ожидает результатов его выполнения и получает значение, работать с которым можно в коде тела цикла. Это значение выводится в сообщении. Этот процесс повторяется. Корутина main() приостанавливается, выполняется очередной шаг итератора, который тоже приостанавливается, после чего возобновляется работа main(). Так происходит до тех пор, пока итератор не выдаст все имеющиеся у него значения. После того, как внутренний счётчик итератора дойдёт до 10 — вызывается исключение StopAsyncIteration. Но это не приводит к аварийному завершению работы программы. Вместо этого система ожидает появления этого исключения. Оно обрабатывается циклом async for, после чего цикл прекращается. Здесь был продемонстрирован обход асинхронного итератора с использованием цикла async for. Подробности об асинхронных итераторах ищите здесь. Наша следующая тема — асинхронные генераторы. Генераторы — это неотъемлемая часть Python. Генератор — это функция, имеющая как минимум одно выражение yield. Выполнение таких функций, как и корутин, можно приостанавливать и возобновлять. На самом деле, Python-корутины — это расширение Python-генераторов. Модуль asyncio позволяет разрабатывать асинхронные генераторы. Асинхронный генератор можно создать, определив корутину, которая использует выражение yield. Поговорим об асинхронных генераторах. Асинхронный генератор — это корутина, в которой используется выражение yield. Прежде чем мы углубимся в исследование асинхронных генераторов — вспомним о том, что собой представляют классические Python-генераторы. Генератор — это Python-функция, которая возвращает значения посредством выражения yield: Код генератора выполняется до выражения yield, после чего возвращается значение. В этом месте выполнение генератора приостанавливается. Когда генератор будет выполнен в следующий раз — его работа возобновится там, где была приостановлена, после чего его код будет выполняться до следующего выражения yield. Генератор: функция, которая возвращает итерируемый генератор. Он выглядит как обычная функция, за исключением того, что содержит выражение yield, с помощью которого выдаются последовательности значений. Их можно применять в циклах for или получать по одному, пользуясь функцией next(). Python glossary С технической точки зрения, функция-генератор создаёт и возвращает итерируемый генератор. Он выполняет код функции, при необходимости выдавая значения, приостанавливая и возобновляя её работу. Итерируемый генератор: объект, созданный функцией-генератором. Каждое выражение yield временно приостанавливает его работу, при этом запоминается состояние выполнения в том месте, где была приостановлена работа [...]. Когда итерируемый генератор возобновляет работу — он начинает с того места, где остановился. Python glossary Пошаговое выполнение кода генератора можно организовать, воспользовавшись встроенной функцией next(). Например: Правда, чаще генераторы итерируют до тех пор, пока они не перестают выдавать значения. Например — используя цикл for или механизм спискового включения: А теперь вернёмся к асинхронным генераторам. Асинхронный генератор — это корутина, которая использует выражение yield. В отличие от функции-генератора, корутина может планировать и ожидать выполнение других корутин и задач. Асинхронный генератор: функция, которая возвращает асинхронный итерируемый генератор. Она похожа на функцию корутины, определённую с использованием ключевых слов async def, за исключением того, что она содержит выражение yield. Это выражение используется для выдачи последовательности значений, подходящих для использования в цикле async for. Python glossary Функции асинхронных генераторов, как и функции генераторов классических, могут использоваться для создания асинхронных итерируемых генераторов. Их можно обходить с помощью встроенной функции anext(), классический аналог которой носит имя next(). Асинхронный итерируемый генератор: объект, созданный асинхронной функцией-генератором. Это — асинхронный итератор, который, при обращении к нему с использованием метода anext(), возвращает объект, допускающий ожидание, выполняющий тело асинхронной функции-генератора до следующего выражения yield. Python glossary Это значит, что асинхронный итерируемый генератор реализует метод anext(), и что с ним можно работать в цикле async for. Каждая итерация генератора планируется и выполняется в виде объекта, допускающего ожидание. Цикл async for запланирует и выполнит каждую итерацию генератора, приостанавливая вызывающую корутину и ожидая результат. Подробности об async for можно найти здесь. Здесь мы поговорим о том, как определять и создавать асинхронные генераторы, как получать их значения и как их обходить. Начнём с их определения. Определить асинхронный генератор можно, определив корутину, в которой имеется по меньшей мере одно выражение yield. Это значит, что интересующая нас функция определяется с использованием ключевых слов async def. Например: Асинхронный генератор — это корутина. Каждый итератор возвращает объект, допускающий ожидание, выполнение которого планируется в цикле событий asyncio, и который в этом цикле выполняется. Поэтому мы можем выполнять такие объекты и ожидать результаты их работы в теле генератора. Например: Теперь поговорим об использовании асинхронных генераторов. Для использования асинхронного генератора сначала надо создать его экземпляр. Это выглядит как вызов соответствующей функции, которая создаёт и возвращает объект итератора: В ходе выполнения этого кода возвращается значение, представляющее собой асинхронный итератор, имеющее тип, соответствующий асинхронному итерируемому генератору. Продвинуть генератор на один шаг можно, воспользовавшись встроенной функцией anext(). Это похоже на то, как следующее значение классического генератора получают с помощью функции next(). Результатом этого вызова является объект, допускающий ожидание. Поэтому для того чтобы получить значение, которое он выдаёт, нужно подождать завершения его работы. Например: То же самое можно записать и в одну строку: Асинхронные генераторы можно обходить в циклах, пользуясь конструкцией async for, которая, на каждой итерации цикла, автоматически будет ожидать готовности результата. Например: Для обхода асинхронных генераторов можно воспользоваться и асинхронным механизмом спискового включения, прибегнув к циклу async for для сбора результатов, выдаваемых генератором: Исследуем обход асинхронного генератора с использованием цикла async for. В этом примере, основанном на предыдущем примере, мы рассмотрим полный обход генератора с применением async for. Цикл будет автоматически ожидать разрешения каждого объекта, допускающего ожидание, возвращённого из генератора. Полученные значения будут доступны в теле цикла, что, в данном случае, означает возможность их вывода в сообщениях. Это, вероятно, пример реализации того сценария, в котором асинхронные генераторы используются чаще всего. Вот полный код примера: Здесь сначала создаётся корутина main(), представляющая собой точку входа в программу. Эта корутина начинает работу и запускает цикл async for. Создаётся экземпляр асинхронного генератора, а цикл автоматически осуществляет получение его значений, пользуясь встроенной функцией anext() для получения объектов, допускающих ожидание. Получив такой объект, цикл дожидается завершения его работы и получает его значение, с которым можно работать в теле цикла. Это значение выводится на экран. Этот процесс повторяется до тех пор, пока в генераторе не закончатся значения. Это выглядит так: корутина main() приостанавливается, выполняется одна итерация генератора, он приостанавливается, возобновляется выполнение main(). Данный пример демонстрирует обход асинхронного генератора с использованием цикла async for. Здесь можно найти подробности об асинхронных генераторах. В следующем разделе изучим асинхронные менеджеры контекста. Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки. Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь. Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров. Присоединяйтесь к нашей команде.