Менеджеры контекста

Менеджеры контекста позволяют выделять и освобождать ресурсы строго по необходимости. Самый популярный пример использования менеджера контекста - выражение with. Предположим, у вас есть две связанные операции, которые вы хотите исполнить в паре, поместив между ними блок кода. Менеджеры контекста позволяют сделать именно это. Например:
with open('some_file', 'w') as opened_file:
opened_file.write('Hola!')
Код выше открывает файл, записывает в него данные и закрывает файл после этого. При возникновении ошибки при записи данных в файл менеджер контекста попробует его закрыть. Этот код эквивалентен следующему:
file = open('some_file', 'w')
try:
file.write('Hola!')
finally:
file.close()
Сравнив с первым блоком кода, мы можем заметить замену шаблонного кода на with. Основное преимущество использования with - это гарантия закрытия файла вне зависимости от того, как будет завершён вложенный код.
Распространенный паттерн использования контекстных менеджеров - блокирование и разблокирование ресурсов, а также закрытие открытых файлов (как я уже показал выше).
Давайте посмотрим, как мы можем написать свой собственный менеджер контекста. Это позволит нам лучше понять логику его работы.

Контекст-менеджер как класс

Необходимый минимум функциональности контекстного менеджера требует методов __enter__ и __exit__. Давайте напишем свой контекстный менеджер для работы с файлами и изучим основы.
class File(object):
def __init__(self, file_name, method):
self.file_obj = open(file_name, method)
def __enter__(self):
return self.file_obj
def __exit__(self, type, value, traceback):
self.file_obj.close()
Просто определив методы __enter__ и __exit__, мы можем использовать новый контекстный менеджер с with. Давайте попробуем:
with File('demo.txt', 'w') as opened_file:
opened_file.write('Hola!')
Метод __exit__ принимает три аргумента. Они обязательны для любого метода __exit__ класса контекстного менеджера. Давайте обсудим логику работы:
  1. 1.
    with сохраняет метод __exit__ класса File.
  2. 2.
    Следует вызов метода __enter__ класса File.
  3. 3.
    Метод __enter__ открывает файл и возвращает его.
  4. 4.
    Дескриптор файла передается в opened_file.
  5. 5.
    Мы записываем информацию в файл при помощи .write().
  6. 6.
    with вызывает сохраненный __exit__ метод.
  7. 7.
    Метод __exit__ закрывает файл.

Обработка исключений

Мы ещё не успели поговорить об аргументах type, value и traceback метода __exit__. Между четвертым и шестым шагом при возникновении исключения, Python передает тип, значение и обратную трассировку исключения методу __exit__. Это позволяет методу __exit__ выбирать способ закрытия файла и выполнять дополнительные действия при необходимости. В нашем случае, мы не уделяем им особого внимания.
Что если объект файла вызвал исключение? Возможно, мы пытаемся вызывать метод на объекте, который его не поддерживает. Например:
with File('demo.txt', 'w') as opened_file:
opened_file.undefined_function('Hola!')
Давайте разберём шаги, которые выполняет with при возникновении исключения:
  1. 1.
    Тип, значение и обратная трассировка ошибки передается в метод
    __exit__.
  2. 2.
    Обработка исключения передается методу __exit__
  3. 3.
    Если __exit__ возвращает True, то исключение было корректно обработано.
  4. 4.
    При возврате любого другого значения with вызывает исключение.
В нашем случае метод __exit__ возвращает None (при отсутствии выражения return метод в Python возвращает None). Таким образом, with вызывает исключение:
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
AttributeError: 'file' object has no attribute 'undefined_function'
Давайте попробуем обработать исключение в методе __exit__:
class File(object):
def __init__(self, file_name, method):
self.file_obj = open(file_name, method)
def __enter__(self):
return self.file_obj
def __exit__(self, type, value, traceback):
print("Исключение было обработано")
self.file_obj.close()
return True
with File('demo.txt', 'w') as opened_file:
opened_file.undefined_function()
# Вывод: Исключение было обработано
Наш метод __exit__ возвращает True, таким образом with не вызывает исключение.
Это не единственный способ реализации контекстных менеджеров - есть и другой и мы посмотрим на него в следующем параграфе.

Контекст-менеджер из генератора

Мы также можем реализовать менеджер контекста через декораторы и генераторы. В Python присутствует модуль contextlib специально для этой цели. Вместо написания класса, мы можем реализовать менеджер контекста из функции-генератора. Посмотрим на простой пример:
from contextlib import contextmanager
@contextmanager
def open_file(name):
f = open(name, 'w')
yield f
f.close()
Отлично! Реализация менеджера контекста таким способом смотрится более интуитивной и простой. Тем не менее, этот метод требует определённых знаний о генераторах, yield и декораторах. В примере выше мы не обрабатываем возможные исключения. В целом, он почти такой же, что и предыдущий.
Давайте чуть подробнее разберем этот подход:
  1. 1.
    Python встречает ключевое слово yield. Благодаря этому он создает
    генератор, а не простую функцию.
  2. 2.
    Благодаря декоратору, contextmanager вызывается с функцией
    open_file в качестве аргумента.
  3. 3.
    Функция contextmanager возвращает генератор, обёрнутый в объект
    GeneratorContextManager.
  4. 4.
    GeneratorContextManager присваивается функции open_file. Таким
    образом, когда мы вызовем функцию open_file в следующий раз, то
    фактически обратимся к объекту GeneratorContextManager.
Теперь, когда мы знаем всё это, мы можем использовать созданный менеджер контекста следующим образом:
with open_file('some_file') as f:
f.write('hola!')