Менеджеры контекста позволяют выделять и освобождать ресурсы строго по
необходимости. Самый популярный пример использования менеджера контекста -
выражение 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__
класса контекстного менеджера. Давайте обсудим логику работы:
with
сохраняет метод__exit__
классаFile
.- Следует вызов метода
__enter__
классаFile
. - Метод
__enter__
открывает файл и возвращает его. - Дескриптор файла передается в
opened_file
. - Мы записываем информацию в файл при помощи
.write()
with
вызывает сохраненный__exit__
метод.- Метод
__exit__
закрывает файл.
Мы ещё не успели поговорить об аргументах type
, value
и traceback
метода __exit__
. Между четвертым и шестым шагом при возникновении
исключения, Python передает тип, значение и обратную трассировку исключения
методу __exit__
. Это позволяет методу __exit__
выбирать способ закрытия
файла и выполнять дополнительные действия при необходимости. В нашем случае,
мы не уделяем им особого внимания.
Что если объект файла вызвал исключение? Возможно, мы пытаемся вызывать метод на объекте, который его не поддерживает. Например:
with File('demo.txt', 'w') as opened_file:
opened_file.undefined_function('Hola!')
Давайте разберём шаги, которые выполняет with
при возникновении
исключения.
- Тип, значение и обратная трассировка ошибки передается в метод
__exit__
. - Обработка исключения передается методу
__exit__
- Если
__exit__
возвращаетTrue
, то исключение было корректно обработано. - При возврате любого другого значения
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
и декораторах. В примере выше мы не
обрабатываем возможные исключения. В целом, он почти такой же, что и
предыдущий.
Давайте чуть подробнее разберем этот подход:
- Python встречает ключевое слово
yield
. Благодаря этому он создает генератор, а не простую функцию. - Благодаря декоратору,
contextmanager
вызывается с функциейopen_file
в качестве аргумента. - Функция
contextmanager
возвращает генератор, обёрнутый в объектGeneratorContextManager
. GeneratorContextManager
присваивается функцииopen_file
. Таким образом, когда мы вызовем функциюopen_file
в следующий раз, то фактически обратимся к объектуGeneratorContextManager
.
Теперь, когда мы знаем всё это, мы можем использовать созданный менеджер контекста следующим образом:
with open_file('some_file') as f:
f.write('hola!')