Менеджеры контекста позволяют выделять и освобождать ресурсы строго по необходимости. Самый популярный пример использования менеджера контекста - выражение 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_objdef __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_objdef __exit__(self, type, value, traceback):print("Исключение было обработано")self.file_obj.close()return Truewith File('demo.txt', 'w') as opened_file:opened_file.undefined_function()# Вывод: Исключение было обработано
Наш метод __exit__
возвращает True
, таким образом with
не вызывает исключение.
Это не единственный способ реализации контекстных менеджеров - есть и другой и мы посмотрим на него в следующем параграфе.
Мы также можем реализовать менеджер контекста через декораторы и генераторы. В Python присутствует модуль contextlib
специально для этой цели. Вместо написания класса, мы можем реализовать менеджер контекста из функции-генератора. Посмотрим на простой пример:
from contextlib import contextmanager@contextmanagerdef open_file(name):f = open(name, 'w')yield ff.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!')