Публикуем седьмую часть (1, 2, 3, 4, 5, 6) перевода руководства по модулю asyncio в Python. Здесь представлены разделы исходного материала с 17 по 19. Менеджер контекста в Python — это сущность, которая даёт возможность пользоваться средой, напоминающей блок try-finally. Она, при этом, предусматривает применение единообразного интерфейса и удобного синтаксиса. Например, речь идёт об использовании выражения with. Менеджер контекста обычно используется при работе с некими ресурсами. Он обеспечивает то, что ресурс всегда будет закрыт или освобождён после завершения работы с ним, независимо от того, успешным ли было взаимодействие с ресурсом, или оно завершилось неудачно, с выдачей исключения. Модуль asyncio позволяет разрабатывать асинхронные менеджеры контекста. В asyncio-программах можно создавать и использовать асинхронные менеджеры контекста, определяя объекты, реализующие методы __aenter__() и __aexit__() в виде корутин. Асинхронный менеджер контекста в Python — это объект, реализующий методы __aenter__() и __aexit__(). Прежде чем мы углубимся в подробности об асинхронных менеджерах контекста — поговорим о классических менеджерах контекста. Менеджер контекста — это Python-объект, реализующий методы __enter__() и __exit__(). Менеджер контекста — это объект, который определяет контекст времени выполнения, который должен быть создан при выполнении команды with. Менеджер контекста обрабатывает вход в целевой контекст времени выполнения для выполнения блока кода, а так же выход из этого контекста. With Statement Context Managers Метод __enter__() определяет то, что происходит в начале блока. Например — это может быть открытие или подготовка к работе ресурса наподобие файла, сокета или пула потоков. Метод __exit__() определяет то, что происходит при выходе из блока. Например — закрытие подготовленного ресурса. Типичное использование менеджеров контекста включает в себя сохранение и восстановление различных видов глобального состояния, блокировку и разблокировку ресурсов, закрытие открытых файлов и тому подобное. With Statement Context Managers Менеджеры контекста используются посредством применения выражения with. Обычно объект менеджера контекста создаётся в начале выражения with, а метод __enter__() вызывается автоматически. В теле блока with ресурс используется с помощью именованного объекта менеджера контекста. После этого, при нормальном или аварийном (с выдачей исключения) выходе из блока, автоматически вызывается метод __exit__(). Например: Эта схема работы воспроизводит конструкцию try-finally: Вернёмся к асинхронным менеджерам контекста. Асинхронные менеджеры контекста были представлены в PEP 492 – Coroutines with async and await syntax. Они представляют собой менеджеры контекста, работа которых, при входе в них и выходе из них, может быть приостановлена. Асинхронный менеджер контекста — это менеджер контекста, который может приостанавливать выполнение в методах aenter и aexit. Asynchronous Context Managers Методы __aenter__() и __aexit__() определяются как корутины, а вызывающая сторона дожидается завершения их работы. Достигается это путём использования выражения async with. Подробности об этом выражении можно найти здесь. В результате получается, что асинхронными менеджерами контекста можно пользоваться только в пределах asyncio-программ, так же, как и обычными корутинами. Конструкция async with предназначена для создания и использования асинхронных менеджеров контекста. Это — расширение конструкции with, предназначенное для использования в корутинах, внутри asyncio-программ. Выражение async with похоже на выражение with, используемое при работе с обычными менеджерами контекста. Главное отличие заключается в том, что async with позволяет работать с асинхронными менеджерами контекста в корутинах. Для того чтобы лучше разобраться с конструкцией async with — присмотримся поближе к менеджерам контекста. Эта конструкция позволяет корутинам создавать и использовать асинхронную версию менеджера контекста. Например: Вот — эквивалент этого кода, сконструированный с использованием try-finally: Обратите внимание на то, что тут реализован почти такой же паттерн, который используется при работе с традиционными менеджерами контекста, за исключением того, что создание и закрытие менеджера контекста включает в себя ожидание завершения работы корутин. При этом приостанавливается выполнение текущей корутины, планируется выполнение новой корутины, после чего идёт ожидание завершения её работы. Это ведёт к тому, что асинхронные менеджеры контекста должны реализовывать методы __aenter__() и __aexit__(), при определении которых необходимо использовать конструкцию async def. Благодаря этому они и сами являются корутинами, в которых можно использовать ключевое слово wait. В этом разделе мы поговорим о том, как определять, создавать и использовать асинхронные менеджеры контекста в asyncio-программах. Асинхронный менеджер контекста можно определить в виде Python-объекта, который реализует методы __aenter__() и __aexit__(). Важно то, что оба метода должны быть определены как корутины, с использованием ключевых слов async def, а значит — должны возвращать объекты, допускающие ожидание. Например: Из-за того, что эти методы являются корутинами — они и сами могут дожидаться выполнения неких корутин и задач. Например: Для того чтобы воспользоваться асинхронным менеджером контекста — нужно применить выражение async with. Это приведёт к автоматическому ожиданию завершения работы «входной» и «выходной» корутин, к приостановке, при необходимости, вызывающей корутины. Например: Получается, что, если говорить в более общем смысле, выражение async with и асинхронные менеджеры контекста могут использоваться в asyncio-программах, например — внутри корутин. Теперь, когда мы знаем о том, как пользоваться асинхронными менеджерами контекста — рассмотрим пример. Исследуем работу с асинхронным менеджером контекста посредством выражения async with. В этом примере, основанном на одном из предыдущих подобных примеров, мы воспользуемся асинхронным менеджером контекста так, как им обычно пользуются. Мы применим выражение async with и, в одной строке кода, создадим менеджер контекста и войдём в него. Это приведёт к автоматическому ожиданию завершения «входного» метода менеджера. Затем, во внутреннем блоке кода, мы можем воспользоваться менеджером. В данном случае мы просто выводим сообщение. Выход из внутреннего блока кода приводит к автоматическому запуску ожидания завершения «выходного» метода менеджера контекста. Если сравнить этот пример с предыдущим подобным примером — можно заметить — как много всего автоматически выполняется конструкцией async with. Вот код примера: При запуске этой программы создаётся главная корутина, custom_coroutine(), которая используется в качестве точки входа в asyncio-программу. Главная корутина запускается и создаёт, в выражении async with, экземпляр нашего класса AsyncContextManager. Это выражение автоматически вызывает метод __aenter__() и ожидает завершения работы корутины, которой он является. Выводится сообщение, корутина на некоторое время блокируется. Главная корутина возобновляет работу и выполняет тело менеджера контекста, выводя сообщение. Далее — программа выходит из блока тела менеджера и вызывающая сторона ожидает завершения работы метода __aexit__(), который выводит сообщение и на некоторое время «засыпает». Здесь показана обычная схема использования асинхронных менеджеров контекста в asyncio-программах. Подробности об асинхронных менеджерах контекста можно найти здесь. Теперь обсудим асинхронные comprehension-выражения. Comprehension-выражения — такие, как списковые включения и компактные выражения для создания словарей — это одна из возможностей Python, которая выглядит, как нечто весьма «питонистическое». Эти выражения представляют собой разновидность циклов, устройство которых отличается от циклов, применяемых во многих других языках. Comprehension-выражения позволяют обходить асинхронные генераторы и итераторы, используя конструкцию async for. Асинхронные comprehension-выражения — это асинхронная версия классических comprehension-выражений. Модуль asyncio поддерживает два типа асинхронных comprehension-выражения. Это — выражения, в которых используется async for, и выражения, в которых используется await. PEP 530 добавляет в язык поддержку использования async for в comprehension-выражениях и в генераторах для списков, множеств и словарей. PEP 530: Asynchronous Comprehensions, what’s new in Python 3.6. Прежде чем мы поговорим об асинхронных конструкциях — вспомним о том, как устроены классические comprehension-выражения. Comprehension-выражения позволяют создавать коллекции данных, вроде списков, словарей и множеств, используя компактные синтаксические конструкции. Списковые включения позволяют создавать списки, пользуясь компактными конструкциями. List Comprehensions Списковое включение позволяет создавать списки на основе выражений, содержащих ключевое слово for, записываемых в той же строке, в которой создаётся список. Например: Comprehension-выражения, кроме того, можно использовать для создания словарей и множеств. Асинхронные comprehension-выражения позволяют создавать списки, множества и словари с использованием конструкции async for с асинхронными итерируемыми объектами. Мы предлагаем разрешить использование async for внутри comprehension-выражений для создания списков, множеств и словарей. PEP 530 – Asynchronous Comprehensions Например: В ходе работы этой конструкции будут создаваться корутины или задачи, система будет дожидаться завершения их выполнения, а полученные от них результаты будут размещаться в списке. Вспомните о том, что выражение async for можно использовать только внутри корутин и задач. Кроме того, хочу напомнить, что асинхронный итератор — это итератор, который выдаёт объекты, допускающие ожидание. Выражение async for позволяет вызывающей стороне обойти асинхронный итератор, содержащий объекты, допускающие ожидание, и получить результат от каждого из таких объектов. В реализации конструкции async for осуществляется автоматическое разрешение каждого объекта, допускающего ожидание, или ожидание завершения его работы. Здесь, при необходимости, планируется выполнение корутин. Асинхронный генератор автоматически реализует методы асинхронного итератора. Такие генераторы тоже можно использовать в асинхронных comprehension-выражениях. Например: Выражение await тоже можно использовать в comprehension-выражениях для создания списков, множеств или словарей. Такие конструкции называют comprehension-выражениями с await. Мы предлагаем разрешить использование выражений await и в асинхронных, и в синхронных comprehension-выражениях. PEP 530 – Asynchronous Comprehensions Так же, как и в случае с уже рассмотренными асинхронными comprehension-выражениями, такими выражениями с ключевым словом await можно пользоваться только внутри asyncio-корутин или задач. Это позволяет создавать структуры данных, вроде списков, приостанавливая вызывающую сторону и ожидая результатов от наборов объектов, допускающих ожидание. Например: В ходе работы этой конструкции будет создан список результатов, полученных после поочерёдного ожидания завершения работы каждого из объектов, допускающих ожидание. Текущая корутина будет приостановлена для того чтобы система могла бы последовательно выполнить объекты, допускающие ожидание. Это отличается от конкурентного выполнения таких объектов с использованием функции asyncio.gather(). Возможно, такой подход окажется медленнее, чем применение этой функции. Подробнее об асинхронных comprehension-выражениях можно почитать здесь. Далее — поговорим о выполнении внешних команд из asyncio-программ с использованием подпроцессов. Из программ, основанных на asyncio, можно выполнять внешние команды. Такие команды запускаются в подпроцессах, что позволяет организовать работу, используя неблокирующие операции ввода/вывода. Класс asyncio.subprocess.Process даёт нам представление подпроцесса, выполняемого asyncio. Этот класс предоставляет средства управления подпроцессами в asyncio-программах, позволяя выполнять с ними различные действия, наподобие ожидания результатов их работы и их остановки. Класс Process — это высокоуровневая обёртка, которая позволяет взаимодействовать с подпроцессами и ожидать их завершения. Interacting with Subprocesses API этого класса очень похож на API класса multiprocessing.Process. Возможно, ещё больше он похож на API класса subprocess.Popen. В частности, речь идёт о таких методах, похожих на методы класса subprocess.Popen, как wait(), communicate() и send_signal(), а так же о схожих атрибутах, наподобие stdin, stdout и stderr. Теперь, когда мы узнали о том, что собой представляет класс asyncio.subprocess.Process, поговорим о том, как пользоваться им в asyncio-программах. Для начала отмечу, что экземпляры asyncio.subprocess.Process не создаются напрямую. Вместо этого, при запуске подпроцесса в asyncio-программе, экземпляр этого класса создаётся автоматически. Объект, который служит обёрткой для процессов операционной системы, созданных функциями create_subprocess_exec() и create_subprocess_shell(). Interacting with Subprocesses Есть два инструмента для запуска внешней команды в виде подпроцесса и получения экземпляра класса Process: Функция asyncio.create_subprocess_exec() для прямого запуска команд. Функция asyncio.create_subprocess_shell() для запуска команд с помощью командной оболочки. Команда — это программа, запускаемая в командной строке (в терминале, или в приглашении командной строки). Это — другая программа, которая запускается напрямую. Вот несколько примеров подобных команд в Linux и macOS: ls — вывод содержимого директории. cat — вывод содержимого файла. date — вывод даты. echo — вывод строки, переданной этой команде. sleep — переход в режим ожидания на заданное количество секунд. Этот список можно продолжать. Из asyncio-программ можно выполнять подобные команды, пользуясь функцией create_subprocess_exec(). Эта функция принимает команду и выполняет её напрямую. Это — полезный приём, так как он позволяет командам выполняться в подпроцессах, а корутинам позволяет взаимодействовать с этими подпроцессами. Так как все функции asyncio, направленные на работу с подпроцессами, асинхронны, и asyncio предоставляет множество инструментов для работы с подобными функциями, легко организовать параллельный запуск и мониторинг множество подпроцессов. Asyncio Subprocesses Функция asyncio.create_subprocess_exec(), в отличие от функции asyncio.create_subprocess_shell(), не выполняет команды с использованием командной оболочки. Это значит, что возможности, предоставляемые командной оболочкой, вроде переменных среды, скриптов и подстановочных символов, при прямом выполнении команды будут недоступны. Это, кроме того, означает, что выполнение команд может быть более безопасным, так как при таком подходе отсутствует риск атаки путём внедрения вредоносного кода в оболочку. Теперь, когда мы уже кое-что знаем о функции asyncio.create_subprocess_exec(), поговорим о том, как ей пользоваться. Функция asyncio.create_subprocess_exec() запускает переданные ей команды в подпроцессах. Она принимает имена команд в виде строк. Эта функция возвращает объект asyncio.subprocess.Process, представляющий подпроцесс. Класс Process — это высокоуровневая обёртка, которая позволяет взаимодействовать с подпроцессами и ожидать их завершения. Interacting with Subprocesses Функция asyncio.create_subprocess_exec() — это корутина. То есть — при её использовании необходимо дождаться завершения её работы с помощью await. Функция возвращает значение тогда, когда подпроцесс запустится, а не тогда, когда его работа завершится. Например: Аргументы для запускаемой команды нужно предоставить функции asyncio.create_subprocess_exec() в виде её аргументов, которые следуют за именем команды. Например: Для того чтобы дождаться завершения подпроцесса — можно организовать ожидание завершения работы метода wait() полученного объекта: Можно напрямую остановить подпроцесс, воспользовавшись методами terminate() или kill(), что приведёт к передаче подпроцессу соответствующего сигнала: Работать с входными и выходными данными команды можно, пользуясь stdin, stderr и  stdout. Можно сделать так, чтобы asyncio-программа обрабатывала бы входные или выходные данные подпроцесса. Сделать это можно, указав входной или выходной поток, и задав константу для организации перенаправления данных, такую, как asyncio.subprocess.PIPE. Например, можно перенаправить вывод команды и передать его asyncio-программе: После этого можно прочесть выходные данные программы, обратившись к экземпляру класса asyncio.subprocess.Process с помощью метода communicate(). Этот метод представляет собой корутину, завершения его работы нужно дожидаться. Он используется и для отправки данных подпроцессу, и для получения данных от него. Например: Можно и отправлять данные подпроцессу, пользуясь методом communicate() и устанавливая его аргумент input в значение, соответствующее передаваемым байтам данных: Благодаря применению asyncio.subprocess.PIPE процесс настраивается на использование объекта StreamReader или StreamWriter для, соответственно, чтения данных из подпроцесса и их записи в него. А метод communicate() будет читать или записывать данные, работая с соответствующим предварительно настроенным объектом. Если значение PIPE передано в аргументе stdin — атрибут Process.stdin будет указывать на экземпляр StreamWriter. Если значение PIPE передано в аргументе stdout или stderr — атрибуты Process.stdout и Process.stderr будут указывать на экземпляры StreamReader. Asyncio Subprocesses С объектами StreamReader и StreamWriter можно взаимодействовать, напрямую обращаясь к атрибутам stdin, stdout и stderr объекта, представляющего подпроцесс: Теперь, когда мы знаем о том, как работает функция create_subprocess_exec(), рассмотрим пример работы с ней. Исследуем возможность запуска команд в подпроцессах из asyncio-программ. В этом примере мы запустим в подпроцессе команду echo, которая выводит переданную ей строку. Эта команда выведет строку непосредственно в поток стандартного вывода. Обратите внимание на то, что для успешного запуска этого примера нужно, чтобы у вас был бы доступ к команде echo. Поэтому, если вы работаете в Windows — я не уверен, что этот код у вас запустится. Вот код примера: При запуске этого кода сначала создаётся корутина main(), которая служит точкой входа в asyncio-программу. Эта корутина запускается и вызывает create_subprocess_exec() для выполнения команды. Корутина приостанавливается на время создания подпроцесса. После этого в её распоряжении оказывается экземпляр класс Process. Главная корутина возобновляет работу и выводит сведения о подпроцессе. Процесс main() завершает работу, завершает работу и asyncio-программа. Вывод команды echo появляется в командной строке. Здесь показана методика запуска команд из asyncio-программ. Команды можно выполнять и с использованием командной оболочки ОС. Командная оболочка — это пользовательский интерфейс для командной строки, называемый интерпретатором командной строки (Command Line Interpreter, CLI). CLI интерпретирует и выполняет команды по поручению пользователя. Интерпретатор командной строки, кроме того, предлагает пользователю определённые возможности. Среди них — простой язык программирования для написания скриптов, работа с подстановочными символами, использование конвейеров, переменные среды (вроде PATH) и многое другое. Например, можно перенаправить вывод одной команды на вход другой команды. Скажем — можно отправить содержимое файла /etc/services команде wc, подсчитывающей количество строк, слов и байт в файлах, которая подсчитает количество строк: Среди командных оболочек, используемых в Unix-подобных операционных системах, можно отметить sh, bash, zsh. Есть и множество других оболочек. В Windows роль командной оболочки, надо полагать, играет cmd.exe. Во время работы Python-программы оболочка уже будет работать, так как она уже была использована для запуска этой программы. Для того чтобы получить доступ к оболочке, ничего особенного делать не нужно. Функция asyncio.create_subprocess_shell(), доступная в asyncio-программах, позволяет выполнять команды в текущей оболочке пользователя. Это — полезная возможность, так как она не только позволяет выполнять команды, но и открывает доступ к функционалу командной оболочки, к такому, например, как перенаправление ввода/вывода и подстановочные символы. … указанная команда будет выполнена с помощью командной оболочки. Это может пригодиться в том случае, если Python используется, в основном, из-за того, что он даёт возможность контролировать выполнение потоков команд лучше, чем это делает большинство командных оболочек, но при этом нужен удобный доступ к другим возможностям командной оболочки, к таким, как конвейеры, подстановочные символы при работе с именами файлов, расширение переменных среды, применение символа ~ для обращения к домашней директории пользователя. subprocess — Subprocess management Команда будет выполнена в подпроцессе процесса, выполняющего asyncio-программу. Тут важно то, что asyncio-программа может взаимодействовать с подпроцессом асинхронно, то есть — через корутины. Так как все функции asyncio, направленные на работу с подпроцессами, асинхронны, и asyncio предоставляет множество инструментов для работы с подобными функциями, легко организовать параллельный запуск и мониторинг множество подпроцессов. Asyncio Subprocesses При выполнении команд посредством командной оболочки, а не напрямую, может понадобиться проанализировать безопасность применяемого решения. Дело в том, что при таком подходе имеется как минимум один уровень абстракции и интерпретации между запросом на выполнение команды и выполняемой командой. Это означает возможность атаки путём внедрения в оболочку вредоносного кода. Важно то, что в сферу ответственности приложения входит обеспечение правильного экранирования всех пробелов и специальных символов с целью защиты от уязвимостей, предусматривающих внедрение в оболочку вредоносного кода. Asyncio Subprocesses Теперь, когда мы познакомились с функцией asyncio.create_subprocess_shell() — поговорим о том, как ей пользоваться. Функция asyncio.create_subprocess_shell() выполняет переданную ей текстовую команду в текущей командной оболочке. Она возвращает объект asyncio.subprocess.Process, представляющий процесс. Эта функция очень похожа на функцию asyncio.create_subprocess_exec(), о которой мы говорили выше. Но, всё равно, мы поговорим о том, как пользоваться этой функцией, и о том, как взаимодействовать с процессом через экземпляр класса Process (на тот случай, если вы предыдущий раздел пропустили и попали сразу сюда). Функция create_subprocess_shell() — это корутина. То есть — завершения её работы нужно дожидаться. Она возвращает значение после того, как подпроцесс запустится, а не после того, как он завершится. Например: Дождаться завершения подпроцесса можно, воспользовавшись ключевым словом await и подождав завершения работы метода wait(): Остановить подпроцесс можно по нашей инициативе, примерив метод terminate() или kill(), что приведёт к передаче подпроцессу соответствующего сигнала. Входные и выходные данные команды будут обрабатываться оболочкой, в частности, речь идёт о стандартных потоках stdin, stderr и stdout. При этом можно сделать так, чтобы входными и выходными данными подпроцесса управляла бы asyncio-программа. Достичь этого можно, задавая входные и выходные потоки и указывая константу для перенаправления данных, такую, как asyncio.subprocess.PIPE. Вот, например, как перенаправить в asyncio-программу вывод команды: После этого можно прочесть выходные данные программы, обратившись к методу communicate() экземпляра asyncio.subprocess.Process. Этот метод представляет собой корутину, поэтому необходимо дождаться завершения его работы. Он, при работе с подпроцессом, используется и для отправки, и для получения данных. Например: С помощью этого же метода данные можно отправлять подпроцессам. Делается это путём записи в аргумент input байтового представления передаваемого значения: При использовании asyncio.subprocess.PIPE подпроцесс настраивается на применение StreamReader или StreamWriter для чтения или записи данных. А метод communicate() будет читать или записывать данные, пользуясь экземпляром соответствующего объекта. Если значение PIPE передано в аргументе stdin — атрибут Process.stdin будет указывать на экземпляр StreamWriter. Если значение PIPE передано в аргументе stdout или stderr — атрибуты Process.stdout и Process.stderr будут указывать на экземпляры StreamReader. Asyncio Subprocesses Взаимодействовать с объектами StreamReader и StreamWriter можно напрямую, обращаясь к атрибутам stdin, stdout и stderr объекта, представляющего подпроцесс: Теперь, после знакомства с возможностями функции create_subprocess_shell(), поговорим о её практическом использовании. Исследуем возможность запуска команд из asyncio-программ с использованием командной оболочки. Здесь мы запустим команду echo, выводящую переданную ей строку. Она выводит переданную ей строку напрямую в стандартный поток вывода. Для того чтобы код этого примера заработал, нужен доступ к команде echo. Я не уверен в том, что он будет работать в Windows. Вот код примера: При запуске этого кода сначала создаётся корутина main() — точка входа в программу. Эта корутина запускается и вызывает функцию create_subprocess_shell() для выполнения команды. Затем корутина приостанавливается и ожидает создания подпроцесса. Функция возвращает экземпляр класса Process. Далее — корутина main() возобновляет работу и выводит сведения о подпроцессе. Процесс main() завершается, завершается и asyncio-программа. Вывод команды echo появляется в командной строке. Этот пример демонстрирует работу с командной оболочкой из asyncio-программ. Наша следующая тема — неблокирующие потоки. Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки. Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь. Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров. Присоединяйтесь к нашей команде.