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

Менеджеры контекста позволяют выделять и освобождать ресурсы строго по необходимости. Самый популярный пример использования менеджера контекста - выражение 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. with сохраняет метод __exit__ класса File.

  2. Следует вызов метода __enter__ класса File.

  3. Метод __enter__ открывает файл и возвращает его.

  4. Дескриптор файла передается в opened_file.

  5. Мы записываем информацию в файл при помощи .write().

  6. with вызывает сохраненный __exit__ метод.

  7. Метод __exit__ закрывает файл.

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

Мы ещё не успели поговорить об аргументах type, value и traceback метода __exit__. Между четвертым и шестым шагом при возникновении исключения, Python передает тип, значение и обратную трассировку исключения методу __exit__. Это позволяет методу __exit__ выбирать способ закрытия файла и выполнять дополнительные действия при необходимости. В нашем случае, мы не уделяем им особого внимания.

Что если объект файла вызвал исключение? Возможно, мы пытаемся вызывать метод на объекте, который его не поддерживает. Например:

with File('demo.txt', 'w') as opened_file:
    opened_file.undefined_function('Hola!')

Давайте разберём шаги, которые выполняет with при возникновении исключения:

  1. Тип, значение и обратная трассировка ошибки передается в метод

    __exit__.

  2. Обработка исключения передается методу __exit__

  3. Если __exit__ возвращает True, то исключение было корректно обработано.

  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. Python встречает ключевое слово yield. Благодаря этому он создает

    генератор, а не простую функцию.

  2. Благодаря декоратору, contextmanager вызывается с функцией

    open_file в качестве аргумента.

  3. Функция contextmanager возвращает генератор, обёрнутый в объект

    GeneratorContextManager.

  4. GeneratorContextManager присваивается функции open_file. Таким

    образом, когда мы вызовем функцию open_file в следующий раз, то

    фактически обратимся к объекту GeneratorContextManager.

Теперь, когда мы знаем всё это, мы можем использовать созданный менеджер контекста следующим образом:

with open_file('some_file') as f:
    f.write('hola!')

Last updated