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

Инструкция with в языке Python часто заводит в тупик тех, кто только начинает своё знакомство с  этим языком. Менеджер контекста, создаваемый инструкцией with, таит в себе невиданную мощь и очень полезный функционал, позволяющий безопасно использовать ресурсы, требуемые получения и обязательного освобождения(завершения). Однако, творческий подход позволяет с удобством использовать этот инструмент и в других областях.

Как это работает?

Рассмотрим задачу записи данных в последовательный порт. Для решения этой задачи с применением контекста используют следующий код:

import serial
with serial.Serial(port='COM3') as s:
          s.write('Привет')
          s.write('Как дела')

Здесь мы определяем псевдоним s и используем его в контексте serial. В теле контекста может располагаться любой блок кода, в т.ч и другие контексты. Применение менеджера контекста позволяет гарантировать, что ресурс Serial будет закрыт, что удобно, например, в многопоточных приложениях, где ресурс надо быстро освобождать для возможности использования другими потоками. Или банальная ошибка программиста – забыть закрыть метод, тоже решается использованием менеджера контекста.

Зачем это нужно?

В приведённом выше примере вам не нужно вызывать функции open и close объекта serial, потому что их за вас вызовет менеджер контекста. То есть, вам не нужно беспокоится о закрытии порта после выполнения некоторых действий – это сделается автоматически.

Казалось бы совсем не сложно контролировать процесс освобождения ресурса самостоятельно, написав следующее:

import serial

s = serial.Serial(port='COM3')
s.open()
s.write('Привет')
s.write('Как дела?')
s.close()

Однако, есть тут один нюанс. Если вдруг где-то между открытием и закрытием порта произойдёт исключение (exception), то закрытие ресурса не гарантируется!

Конечно, вы можете избежать такой ситуации и модифицировать код следующим образом:

import serial

s = serial.Serial(port='COM3')
s.open()
try:
    s.write('Привет')
    s.write('Как дела?')
finally:
    s.close()

И да, этот код будет абсолютно функционально эквивалентен применению менеджера контекста, он также будет безопасен, но вы можете сделать его проще и лаконичнее с помощью with.

Главная ценность инструкции with это возможность абстрагироваться от функциональности распространенных шаблонов управления ресурсов, упрощая работу с ними.

Как это использовать в собственных разработках?

Возникает закономерный вопрос: “Как инструкция with узнает, какой метод вызывать при открытии контекста и при его завершении”?

Для этого используются “волшебные” методы __enter__ и __exit__ или декоратор, созданный с помощью модуля contextlib. Рассмотрим оба случая.

Собственный класс

Метод __enter__ выполняется при входе в контекст, а метод __exit__ при выходе из него. Кроме безопасного доступа к ресурсам, этот функционал можно использовать для аккуратной реализации не связанных с ресурсами вещей, например, оформления вывода текста.

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

class list:
	def __init__(self):
		self.level = 0

	def __enter__(self):
		self.level += 1
		return self

	def __exit__(self, exc_type, exc_val, exc_tb):
		self.level -= 1

	def write(self, text):
		print('    ' * self.level + '* ' + text)


with list() as l:
	l.write("Фрукты")
	with l:
		l.write("Бананы")
		l.write("Ананасы")

	l.write("Ягоды")
	with l:
		l.write("Черника")
		l.write("Малина")

И в результате выполнения программы мы получим следующий отформатированный список с двумя уровнями вложенности:

    * Фрукты
        * Бананы
        * Ананасы
    * Ягоды
        * Черника
        * Малина

Декоратор

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

from contextlib import contextmanager
import serial

@contextmanager
def comOpen(portname):
    ser = serial.Serial()
    try:
        ser.setPort(portname)
        yield ser
    except serial.serialutil.SerialException:
        print("Ошибочка вышла!")
    finally:
        print('Закрываем порт')
        ser.close()


with comOpen('COM3') as s:
    s.write('Hello!')

И если мы выберем заранее неверный порт, то увидим на экране вывода сообщение:

Ошибочка вышла!
Закрываем порт

Реализация удалась! Менеджер контекста работает!

Резюме

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