Публикуем седьмую часть (12345, 6) перевода руководства по модулю asyncio в Python. Здесь представлены разделы исходного материала с 17 по 19.

17. Асинхронные менеджеры контекста

Менеджер контекста в Python — это сущность, которая даёт возможность пользоваться средой, напоминающей блок try-finally. Она, при этом, предусматривает применение единообразного интерфейса и удобного синтаксиса. Например, речь идёт об использовании выражения with.

Менеджер контекста обычно используется при работе с некими ресурсами. Он обеспечивает то, что ресурс всегда будет закрыт или освобождён после завершения работы с ним, независимо от того, успешным ли было взаимодействие с ресурсом, или оно завершилось неудачно, с выдачей исключения.

Модуль asyncio позволяет разрабатывать асинхронные менеджеры контекста.

В asyncio-программах можно создавать и использовать асинхронные менеджеры контекста, определяя объекты, реализующие методы __aenter__() и __aexit__() в виде корутин.

17.1. Что собой представляет асинхронный менеджер контекста

Асинхронный менеджер контекста в Python — это объект, реализующий методы __aenter__() и __aexit__().

Прежде чем мы углубимся в подробности об асинхронных менеджерах контекста — поговорим о классических менеджерах контекста.

Классические менеджеры контекста

Менеджер контекста — это Python-объект, реализующий методы __enter__() и __exit__().

Менеджер контекста — это объект, который определяет контекст времени выполнения, который должен быть создан при выполнении команды with. Менеджер контекста обрабатывает вход в целевой контекст времени выполнения для выполнения блока кода, а так же выход из этого контекста.

With Statement Context Managers

  • Метод __enter__() определяет то, что происходит в начале блока. Например — это может быть открытие или подготовка к работе ресурса наподобие файла, сокета или пула потоков.

  • Метод __exit__() определяет то, что происходит при выходе из блока. Например — закрытие подготовленного ресурса.

Типичное использование менеджеров контекста включает в себя сохранение и восстановление различных видов глобального состояния, блокировку и разблокировку ресурсов, закрытие открытых файлов и тому подобное.

With Statement Context Managers

Менеджеры контекста используются посредством применения выражения with.

Обычно объект менеджера контекста создаётся в начале выражения with, а метод __enter__() вызывается автоматически. В теле блока with ресурс используется с помощью именованного объекта менеджера контекста. После этого, при нормальном или аварийном (с выдачей исключения) выходе из блока, автоматически вызывается метод __exit__().

Например:

...
# открытие менеджера контекста
with ContextManager() as manager:
    # ...
# закрывается автоматически

Эта схема работы воспроизводит конструкцию try-finally:

...
# создание объекта
manager = ContextManager()
try:
    manager.__enter__()
    # ...
finally:
    manager.__exit__()

Вернёмся к асинхронным менеджерам контекста.

Асинхронные менеджеры контекста

Асинхронные менеджеры контекста были представлены в PEP 492 – Coroutines with async and await syntax.

Они представляют собой менеджеры контекста, работа которых, при входе в них и выходе из них, может быть приостановлена.

Асинхронный менеджер контекста — это менеджер контекста, который может приостанавливать выполнение в методах aenter и aexit.

Asynchronous Context Managers

Методы __aenter__() и __aexit__() определяются как корутины, а вызывающая сторона дожидается завершения их работы.

Достигается это путём использования выражения async with.

Подробности об этом выражении можно найти здесь.

В результате получается, что асинхронными менеджерами контекста можно пользоваться только в пределах asyncio-программ, так же, как и обычными корутинами.

О выражении async with

Конструкция async with предназначена для создания и использования асинхронных менеджеров контекста.

Это — расширение конструкции with, предназначенное для использования в корутинах, внутри asyncio-программ.

Выражение async with похоже на выражение with, используемое при работе с обычными менеджерами контекста. Главное отличие заключается в том, что async with позволяет работать с асинхронными менеджерами контекста в корутинах.

Для того чтобы лучше разобраться с конструкцией async with — присмотримся поближе к менеджерам контекста.

Эта конструкция позволяет корутинам создавать и использовать асинхронную версию менеджера контекста.

Например:

...
# создание и использование асинхронного менеджера контекста
async with AsyncContextManager() as manager:
    # ...

Вот — эквивалент этого кода, сконструированный с использованием try-finally:

...
# создание асинхронного менеджера контекста, или вход в него
manager = await AsyncContextManager()
try:
    # ...
finally:
    # закрытие менеджера контекста, или выход из него
    await manager.close()

Обратите внимание на то, что тут реализован почти такой же паттерн, который используется при работе с традиционными менеджерами контекста, за исключением того, что создание и закрытие менеджера контекста включает в себя ожидание завершения работы корутин.

При этом приостанавливается выполнение текущей корутины, планируется выполнение новой корутины, после чего идёт ожидание завершения её работы.

Это ведёт к тому, что асинхронные менеджеры контекста должны реализовывать методы __aenter__() и __aexit__(), при определении которых необходимо использовать конструкцию async def. Благодаря этому они и сами являются корутинами, в которых можно использовать ключевое слово wait.

17.2. Как пользоваться асинхронными менеджерами контекста

В этом разделе мы поговорим о том, как определять, создавать и использовать асинхронные менеджеры контекста в asyncio-программах.

Определение асинхронных менеджеров контекста

Асинхронный менеджер контекста можно определить в виде Python-объекта, который реализует методы __aenter__() и __aexit__().

Важно то, что оба метода должны быть определены как корутины, с использованием ключевых слов async def, а значит — должны возвращать объекты, допускающие ожидание.

Например:

# определение асинхронного менеджера контекста
class AsyncContextManager:
    # вход в асинхронный менеджер контекста
    async def __aenter__(self):
        # вывод сообщения
        print('>entering the context manager')
 
    # выход из асинхронного менеджера контекста
    async def __aexit__(self, exc_type, exc, tb):
        # вывод сообщения
        print('>exiting the context manager')

Из-за того, что эти методы являются корутинами — они и сами могут дожидаться выполнения неких корутин и задач.

Например:

# определение асинхронного менеджера контекста
class AsyncContextManager:
    # вход в асинхронный менеджер контекста
    async def __aenter__(self):
        # вывод сообщения
        print('>entering the context manager')
        # блокировка на некоторое время
        await asyncio.sleep(0.5)
 
    # выход из асинхронного менеджера контекста
    async def __aexit__(self, exc_type, exc, tb):
        # вывод сообщения
        print('>exiting the context manager')
        # блокировка на некоторое время
        await asyncio.sleep(0.5)

Использование асинхронных менеджеров контекста

Для того чтобы воспользоваться асинхронным менеджером контекста — нужно применить выражение async with.

Это приведёт к автоматическому ожиданию завершения работы «входной» и «выходной» корутин, к приостановке, при необходимости, вызывающей корутины.

Например:

# использование асинхронного менеджера контекста
async with AsyncContextManager() as manager:
    # ...

Получается, что, если говорить в более общем смысле, выражение async with и асинхронные менеджеры контекста могут использоваться в asyncio-программах, например — внутри корутин.

Теперь, когда мы знаем о том, как пользоваться асинхронными менеджерами контекста — рассмотрим пример.

17.3. Пример использования асинхронного менеджера контекста и async with

Исследуем работу с асинхронным менеджером контекста посредством выражения async with.

В этом примере, основанном на одном из предыдущих подобных примеров, мы воспользуемся асинхронным менеджером контекста так, как им обычно пользуются.

Мы применим выражение async with и, в одной строке кода, создадим менеджер контекста и войдём в него. Это приведёт к автоматическому ожиданию завершения «входного» метода менеджера.

Затем, во внутреннем блоке кода, мы можем воспользоваться менеджером. В данном случае мы просто выводим сообщение.

Выход из внутреннего блока кода приводит к автоматическому запуску ожидания завершения «выходного» метода менеджера контекста.

Если сравнить этот пример с предыдущим подобным примером — можно заметить — как много всего автоматически выполняется конструкцией async with.

Вот код примера:

# SuperFastPython.com
# пример работы с асинхронным менеджером контекста и с async with
import asyncio
 
# определение асинхронного менеджера контекста
class AsyncContextManager:
    # вход в асинхронный менеджер контекста
    async def __aenter__(self):
        # вывод сообщения
        print('>entering the context manager')
        # блокировка на некоторое время
        await asyncio.sleep(0.5)
 
    # выход из асинхронного менеджера контекста
    async def __aexit__(self, exc_type, exc, tb):
        # вывод сообщения
        print('>exiting the context manager')
        # блокировка на некоторое время
        await asyncio.sleep(0.5)
 
# определение простой корутины
async def custom_coroutine():
    # создание и использование асинхронного менеджера контекста
    async with AsyncContextManager() as manager:
        # вывод результирующего сообщения
        print(f'within the manager')
 
# запуск asyncio-программы
asyncio.run(custom_coroutine())

При запуске этой программы создаётся главная корутина, custom_coroutine(), которая используется в качестве точки входа в asyncio-программу.

Главная корутина запускается и создаёт, в выражении async with, экземпляр нашего класса AsyncContextManager.

Это выражение автоматически вызывает метод __aenter__() и ожидает завершения работы корутины, которой он является. Выводится сообщение, корутина на некоторое время блокируется.

Главная корутина возобновляет работу и выполняет тело менеджера контекста, выводя сообщение.

Далее — программа выходит из блока тела менеджера и вызывающая сторона ожидает завершения работы метода __aexit__(), который выводит сообщение и на некоторое время «засыпает».

Здесь показана обычная схема использования асинхронных менеджеров контекста в asyncio-программах.

>entering the context manager
within the manager
>exiting the context manager

Подробности об асинхронных менеджерах контекста можно найти здесь.

Теперь обсудим асинхронные comprehension-выражения.

18. Асинхронные comprehension-выражения

Comprehension-выражения — такие, как списковые включения и компактные выражения для создания словарей — это одна из возможностей Python, которая выглядит, как нечто весьма «питонистическое».

Эти выражения представляют собой разновидность циклов, устройство которых отличается от циклов, применяемых во многих других языках.

Comprehension-выражения позволяют обходить асинхронные генераторы и итераторы, используя конструкцию async for.

18.1. Что собой представляют асинхронные comprehension-выражения

Асинхронные comprehension-выражения — это асинхронная версия классических comprehension-выражений.

Модуль asyncio поддерживает два типа асинхронных comprehension-выражения. Это — выражения, в которых используется async for, и выражения, в которых используется await.

PEP 530 добавляет в язык поддержку использования async for в comprehension-выражениях и в генераторах для списков, множеств и словарей.

PEP 530: Asynchronous Comprehensions, what’s new in Python 3.6.

Прежде чем мы поговорим об асинхронных конструкциях — вспомним о том, как устроены классические comprehension-выражения.

18.2. Классические comprehension-выражения

Comprehension-выражения позволяют создавать коллекции данных, вроде списков, словарей и множеств, используя компактные синтаксические конструкции.

Списковые включения позволяют создавать списки, пользуясь компактными конструкциями.

List Comprehensions

Списковое включение позволяет создавать списки на основе выражений, содержащих ключевое слово for, записываемых в той же строке, в которой создаётся список.

Например:

...
# создание списка с использованием спискового включения
result = [a*2 for a in range(100)]

Comprehension-выражения, кроме того, можно использовать для создания словарей и множеств.

Например:
...
# создание словаря с использованием comprehension-выражения
result = {a:i for a,i in zip(['a','b','c'],range(3))}
# создание множества с использованием comprehension-выражения
result = {a for a in [1, 2, 3, 2, 3, 1, 5, 4]}

18.3. Асинхронные comprehension-выражения

Асинхронные comprehension-выражения позволяют создавать списки, множества и словари с использованием конструкции async for с асинхронными итерируемыми объектами.

Мы предлагаем разрешить использование async for внутри comprehension-выражений для создания списков, множеств и словарей.

PEP 530 – Asynchronous Comprehensions

Например:

...
# асинхронное списковое включение, использующее асинхронный итератор
result = [a async for a in aiterable]

В ходе работы этой конструкции будут создаваться корутины или задачи, система будет дожидаться завершения их выполнения, а полученные от них результаты будут размещаться в списке.

Вспомните о том, что выражение async for можно использовать только внутри корутин и задач.

Кроме того, хочу напомнить, что асинхронный итератор — это итератор, который выдаёт объекты, допускающие ожидание.

Выражение async for позволяет вызывающей стороне обойти асинхронный итератор, содержащий объекты, допускающие ожидание, и получить результат от каждого из таких объектов.

В реализации конструкции async for осуществляется автоматическое разрешение каждого объекта, допускающего ожидание, или ожидание завершения его работы. Здесь, при необходимости, планируется выполнение корутин.

Асинхронный генератор автоматически реализует методы асинхронного итератора. Такие генераторы тоже можно использовать в асинхронных comprehension-выражениях.

Например:

...
# асинхронное списковое включение, в котором используется асинхронный генератор
result = [a async for a in agenerator]

18.4. Ключевое слово await и асинхронные comprehension-выражения

Выражение await тоже можно использовать в comprehension-выражениях для создания списков, множеств или словарей. Такие конструкции называют comprehension-выражениями с await.

Мы предлагаем разрешить использование выражений await и в асинхронных, и в синхронных comprehension-выражениях.

PEP 530 – Asynchronous Comprehensions

Так же, как и в случае с уже рассмотренными асинхронными comprehension-выражениями, такими выражениями с ключевым словом await можно пользоваться только внутри asyncio-корутин или задач.

Это позволяет создавать структуры данных, вроде списков, приостанавливая вызывающую сторону и ожидая результатов от наборов объектов, допускающих ожидание.

Например:

...
# списковое включение с выражением await и обработка коллекции объектов, допускающих ожидание
results = [await a for a in awaitables]

В ходе работы этой конструкции будет создан список результатов, полученных после поочерёдного ожидания завершения работы каждого из объектов, допускающих ожидание.

Текущая корутина будет приостановлена для того чтобы система могла бы последовательно выполнить объекты, допускающие ожидание. Это отличается от конкурентного выполнения таких объектов с использованием функции asyncio.gather(). Возможно, такой подход окажется медленнее, чем применение этой функции.

Подробнее об асинхронных comprehension-выражениях можно почитать здесь.

Далее — поговорим о выполнении внешних команд из asyncio-программ с использованием подпроцессов.

19. Выполнение команд в неблокирующих подпроцессах

Из программ, основанных на asyncio, можно выполнять внешние команды.

Такие команды запускаются в подпроцессах, что позволяет организовать работу, используя неблокирующие операции ввода/вывода.

19.1. Что собой представляет класс asyncio.subprocess.Process

Класс asyncio.subprocess.Process даёт нам представление подпроцесса, выполняемого asyncio.

Этот класс предоставляет средства управления подпроцессами в asyncio-программах, позволяя выполнять с ними различные действия, наподобие ожидания результатов их работы и их остановки.

Класс Process — это высокоуровневая обёртка, которая позволяет взаимодействовать с подпроцессами и ожидать их завершения.

Interacting with Subprocesses

API этого класса очень похож на API класса multiprocessing.Process. Возможно, ещё больше он похож на API класса subprocess.Popen.

В частности, речь идёт о таких методах, похожих на методы класса subprocess.Popen, как wait()communicate() и send_signal(), а так же о схожих атрибутах, наподобие stdinstdout и 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() для запуска команд с помощью командной оболочки.

19.2. Прямой запуск команд

Команда — это программа, запускаемая в командной строке (в терминале, или в приглашении командной строки). Это — другая программа, которая запускается напрямую.

Вот несколько примеров подобных команд в 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.create_subprocess_exec() запускает переданные ей команды в подпроцессах. Она принимает имена команд в виде строк.

Эта функция возвращает объект asyncio.subprocess.Process, представляющий подпроцесс.

Класс Process — это высокоуровневая обёртка, которая позволяет взаимодействовать с подпроцессами и ожидать их завершения.

Interacting with Subprocesses

Функция asyncio.create_subprocess_exec() — это корутина. То есть — при её использовании необходимо дождаться завершения её работы с помощью await. Функция возвращает значение тогда, когда подпроцесс запустится, а не тогда, когда его работа завершится.

Например:

...
# выполнение команды в подпроцессе
process = await asyncio.create_subprocess_exec('ls')

Аргументы для запускаемой команды нужно предоставить функции asyncio.create_subprocess_exec() в виде её аргументов, которые следуют за именем команды.

Например:

...
# выполнение в подпроцессе команды с аргументами
process = await asyncio.create_subprocess_exec('ls', '-l')

Для того чтобы дождаться завершения подпроцесса — можно организовать ожидание завершения работы метода wait() полученного объекта:

...
# ожидание завершения подпроцесса
await process.wait()

Можно напрямую остановить подпроцесс, воспользовавшись методами terminate() или kill(), что приведёт к передаче подпроцессу соответствующего сигнала:

...
# завершение подпроцесса
process.terminate()

Работать с входными и выходными данными команды можно, пользуясь stdin, stderr и  stdout.

Можно сделать так, чтобы asyncio-программа обрабатывала бы входные или выходные данные подпроцесса.

Сделать это можно, указав входной или выходной поток, и задав константу для организации перенаправления данных, такую, как asyncio.subprocess.PIPE.

Например, можно перенаправить вывод команды и передать его asyncio-программе:

...
# запуск подпроцесса и перенаправление его вывода
process = await asyncio.create_subprocess_exec('ls', stdout=asyncio.subprocess.PIPE)

После этого можно прочесть выходные данные программы, обратившись к экземпляру класса asyncio.subprocess.Process с помощью метода communicate().

Этот метод представляет собой корутину, завершения его работы нужно дожидаться. Он используется и для отправки данных подпроцессу, и для получения данных от него.

Например:

...
# чтение данных из подпроцесса
line = process.communicate()

Можно и отправлять данные подпроцессу, пользуясь методом communicate() и устанавливая его аргумент input в значение, соответствующее передаваемым байтам данных:

...
# запуск подпроцесса и перенаправление ввода
process = await asyncio.create_subprocess_exec('ls', stdin=asyncio.subprocess.PIPE)
# отправка данных подпроцессу
process.communicate(input=b'Hello\n')

Благодаря применению asyncio.subprocess.PIPE процесс настраивается на использование объекта StreamReader или StreamWriter для, соответственно, чтения данных из подпроцесса и их записи в него. А метод communicate() будет читать или записывать данные, работая с соответствующим предварительно настроенным объектом.

Если значение PIPE передано в аргументе stdin — атрибут Process.stdin будет указывать на экземпляр StreamWriter. Если значение PIPE передано в аргументе stdout или stderr — атрибуты Process.stdout и Process.stderr будут указывать на экземпляры StreamReader.

Asyncio Subprocesses

С объектами StreamReader и StreamWriter можно взаимодействовать, напрямую обращаясь к атрибутам stdinstdout и stderr объекта, представляющего подпроцесс:

...
# чтение строки из выходного потока подпроцесса
line = await process.stdout.readline()

Теперь, когда мы знаем о том, как работает функция create_subprocess_exec(), рассмотрим пример работы с ней.

Пример использования функции asyncio.create_subprocess_exec()

Исследуем возможность запуска команд в подпроцессах из asyncio-программ.

В этом примере мы запустим в подпроцессе команду echo, которая выводит переданную ей строку.

Эта команда выведет строку непосредственно в поток стандартного вывода.

Обратите внимание на то, что для успешного запуска этого примера нужно, чтобы у вас был бы доступ к команде echo. Поэтому, если вы работаете в Windows — я не уверен, что этот код у вас запустится.

Вот код примера:

# SuperFastPython.com
# пример выполнения команды в подпроцессе в asyncio-программе
import asyncio
 
# главная корутина
async def main():
    # начало выполнения команды в подпроцессе
    process = await asyncio.create_subprocess_exec('echo', 'Hello World')
    # вывод сведений о подпроцессе
    print(f'subprocess: {process}')
 
# точка входа в программу
asyncio.run(main())

При запуске этого кода сначала создаётся корутина main(), которая служит точкой входа в asyncio-программу.

Эта корутина запускается и вызывает create_subprocess_exec() для выполнения команды.

Корутина приостанавливается на время создания подпроцесса. После этого в её распоряжении оказывается экземпляр класс Process.

Главная корутина возобновляет работу и выводит сведения о подпроцессе. Процесс main() завершает работу, завершает работу и asyncio-программа.

Вывод команды echo появляется в командной строке.

Здесь показана методика запуска команд из asyncio-программ.

Hello World
subprocess: <Process 50249>

19.3. Запуск команд с помощью командной оболочки

Команды можно выполнять и с использованием командной оболочки ОС.

Командная оболочка — это пользовательский интерфейс для командной строки, называемый интерпретатором командной строки (Command Line Interpreter, CLI).

CLI интерпретирует и выполняет команды по поручению пользователя.

Интерпретатор командной строки, кроме того, предлагает пользователю определённые возможности. Среди них — простой язык программирования для написания скриптов, работа с подстановочными символами, использование конвейеров, переменные среды (вроде PATH) и многое другое.

Например, можно перенаправить вывод одной команды на вход другой команды. Скажем — можно отправить содержимое файла /etc/services команде wc, подсчитывающей количество строк, слов и байт в файлах, которая подсчитает количество строк:

cat /etc/services | wc -l

Среди командных оболочек, используемых в Unix-подобных операционных системах, можно отметить shbashzsh. Есть и множество других оболочек.

В 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.create_subprocess_shell() выполняет переданную ей текстовую команду в текущей командной оболочке.

Она возвращает объект asyncio.subprocess.Process, представляющий процесс.

Эта функция очень похожа на функцию asyncio.create_subprocess_exec(), о которой мы говорили выше. Но, всё равно, мы поговорим о том, как пользоваться этой функцией, и о том, как взаимодействовать с процессом через экземпляр класса Process (на тот случай, если вы предыдущий раздел пропустили и попали сразу сюда).

Функция create_subprocess_shell() — это корутина. То есть — завершения её работы нужно дожидаться. Она возвращает значение после того, как подпроцесс запустится, а не после того, как он завершится.

Например:

...
# запуск подпроцесса
process = await asyncio.create_subprocess_shell('ls')

Дождаться завершения подпроцесса можно, воспользовавшись ключевым словом await и подождав завершения работы метода wait():

...
# ожидание завершения подпроцесса
await process.wait()

Остановить подпроцесс можно по нашей инициативе, примерив метод terminate() или kill(), что приведёт к передаче подпроцессу соответствующего сигнала.

Входные и выходные данные команды будут обрабатываться оболочкой, в частности, речь идёт о стандартных потоках stdinstderr и stdout. При этом можно сделать так, чтобы входными и выходными данными подпроцесса управляла бы asyncio-программа.

Достичь этого можно, задавая входные и выходные потоки и указывая константу для перенаправления данных, такую, как asyncio.subprocess.PIPE.

Вот, например, как перенаправить в asyncio-программу вывод команды:

...
# запуск подпроцесса и перенаправление вывода
process = await asyncio.create_subprocess_shell('ls', stdout=asyncio.subprocess.PIPE)

После этого можно прочесть выходные данные программы, обратившись к методу communicate() экземпляра asyncio.subprocess.Process.

Этот метод представляет собой корутину, поэтому необходимо дождаться завершения его работы. Он, при работе с подпроцессом, используется и для отправки, и для получения данных.

Например:

...
# чтение данных, полученных от подпроцесса
line = process.communicate()

С помощью этого же метода данные можно отправлять подпроцессам. Делается это путём записи в аргумент input байтового представления передаваемого значения:

...
# запуск подпроцесса и перенаправление ввода
process = await asyncio.create_subprocess_shell('ls', stdin=asyncio.subprocess.PIPE)
# отправка данных подпроцессу
process.communicate(input=b'Hello\n')

При использовании asyncio.subprocess.PIPE подпроцесс настраивается на применение StreamReader или StreamWriter для чтения или записи данных. А метод communicate() будет читать или записывать данные, пользуясь экземпляром соответствующего объекта.

Если значение PIPE передано в аргументе stdin — атрибут Process.stdin будет указывать на экземпляр StreamWriter. Если значение PIPE передано в аргументе stdout или stderr — атрибуты Process.stdout и Process.stderr будут указывать на экземпляры StreamReader.

Asyncio Subprocesses

Взаимодействовать с объектами StreamReader и StreamWriter можно напрямую, обращаясь к атрибутам stdinstdout и stderr объекта, представляющего подпроцесс:

...
# чтение строки из выходного потока подпроцесса
line = await process.stdout.readline()

Теперь, после знакомства с возможностями функции create_subprocess_shell(), поговорим о её практическом использовании.

Пример использования функции asyncio.create_subprocess_shell()

Исследуем возможность запуска команд из asyncio-программ с использованием командной оболочки.

Здесь мы запустим команду echo, выводящую переданную ей строку. Она выводит переданную ей строку напрямую в стандартный поток вывода.

Для того чтобы код этого примера заработал, нужен доступ к команде echo. Я не уверен в том, что он будет работать в Windows.

Вот код примера:

# SuperFastPython.com
# пример выполнения команды командной оболочки в asyncio-программе в виде подпроцесса
import asyncio
 
# главная корутина
async def main():
    # начало выполнения команды командной оболочки в подпроцессе
    process = await asyncio.create_subprocess_shell('echo Hello World')
    # вывод сведений о подпроцессе
    print(f'subprocess: {process}')
 
# точка входа в программу
asyncio.run(main())

При запуске этого кода сначала создаётся корутина main() — точка входа в программу. Эта корутина запускается и вызывает функцию create_subprocess_shell() для выполнения команды. Затем корутина приостанавливается и ожидает создания подпроцесса. Функция возвращает экземпляр класса Process.

Далее — корутина main() возобновляет работу и выводит сведения о подпроцессе. Процесс main() завершается, завершается и asyncio-программа.

Вывод команды echo появляется в командной строке.

Этот пример демонстрирует работу с командной оболочкой из asyncio-программ.

subprocess: <Process 43916>
Hello World

Наша следующая тема — неблокирующие потоки.

О, а приходите к нам работать? ???? ????

Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.

Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.

Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.

Присоединяйтесь к нашей команде.

Комментарии (0)