Менеджер контекста with в Python

Наверняка каждый, кто использует в своей работе python, сталкивался с ключевым словом with. Классическим примером будет являться работа с файлами:

with open(path, 'w') as file: file.write(data)

Преимущество использования ключевого слова with перед вызовом функции open() в том, что функция file.close() вызовется автоматически и освободит занятые ресурсы после того, как отработает код. С первого взгляда кажется, что экономится лишь лишняя строка кода в нашей программе, но это не совсем так, главной особенностью конструкции with является то, что финальный код (в данном случае file.close()) вызывается гарантированно, даже в том случае, если при обработке интерпретатором строк внутри конструкции произойдет ошибка.

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

Для примера возьмем следующую задачу – наша программа должна запускать стороннее консольное приложение при помощи модуля subprocess, выполнять измерение времени выполнения этого приложения, а выполнение метода subprocess.terminate() мы сделаем автоматическим и гарантированным.

Код описывающий менеджер контекста:

class ForWith: def __init__(self, a): self.start_time = time.time() self.a = a self.b = subprocess.Popen(self.a) def __enter__(self): return self.b def __exit__(self, *args): print(time.time()-self.start_time) self.b.terminate()

Код для запуска нашего приложения:

program = [path_exe, path_data] with ForWith(program) as p: code = p.wait() print('exit code =', code)

Рассмотрим приведенный выше код:

В созданном нами классе «ForWith» описано три магических метода:

__init__ — служит для описания атрибутов нашего класса, атрибут self.a передает аргументы вызова, self.b – это экземпляр класса subprocess.Popen

__enter__ — в этой функции описываются методы, вызываемые при старте контекстного менеджера. Объект, возвращаемый данной функцией, присваивается переменной в конце выражения with ForWith(*args) as p:. В нашем примере переменной p присвоится атрибут b, который в свою очередь – экземпляр класса subprocess.Popen. Теперь в нашем блоке кода, заключенном в менеджер контекста, мы можем вызывать методы класса subprocess.Popen обращаясь к переменной p, например: p.wait() или p.communicate().

__exit__ — магический метод, который будет вызван в завершении конструкции with, или в случае возникновения ошибки после нее. В этот метод передаются параметры завершения процесса, а код этого метода будет выполнен гарантированно. В нашем примере метод __exit__ выводит на экран время выполнения нашего приложения и вызывает функцию subprocess.terminate(), закрывающую наше приложение и освобождающую ресурсы.

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

1111
5 комментариев

Есть еще один способ создать контекстный менеджер - декоратор @contextmanager из модуля contextlib

https://pastebin.com/wgiB3Yy1

4
Автор

Да, действительно, при помощи этого декоратора можно получить аналогично работающий код. Спасибо за дополнение!

Хорошее объяснение, спасибо!

2

К сожалению, .terminate() не гарантирует завершение процесса.
.terminate() шлет SIGTERM, но SIGTERM - "мягкий" способ завершения, и процесс его может проигнорировать. И в любом случае, SIGTERM берет время на "подумать" и разослать сигналы в родительский и дочерние процессы, если они есть.
По уму бы сделать проверку на фактическое завершение по SIGTERM и в крайнем случае, если процесс не закрылся, убить жёстко через .kill()
И только после этого вычислять время жизни процесса.

1
Автор

Рады помочь!