Урок курса
Создание своего менеджера контекста
Python для профессионаловЧтобы создать собственный контекстный менеджер, класс должен определить два метода:
-
__enter__(self)– код, выполняющийся при входе в блокwith. Его результат будет привязан к имени послеasили проигнорирован, еслиasне используется. -
__exit__(self, exc_type, exc_value, exc_traceback)– код, выполняющийся при выходе из блокаwith. Здесь можно осуществить завершение/очистку ресурса. Если нужно подавить возникшее исключение, этот метод должен вернутьTrue. Обычно же возвращаетсяFalse(или ничего), чтобы исключение далее было возбуждено.
Рассмотрим пример: создадим менеджер контекста, который измеряет время выполнения блока кода.
import time
class Timer:
def __enter__(self):
self.start = time.perf_counter()
return self # вернем сам объект, чтобы им можно было пользоваться внутри блока
def __exit__(self, exc_type, exc_value, exc_traceback):
self.end = time.perf_counter()
self.interval = self.end - self.start
print(f"Блок кода выполнялся {self.interval:.4f} секунд")
# Не подавляем исключения, поэтому возвращаем False (по умолчанию None тоже трактуется как False).
# Использование:
with Timer() as t:
# какой-то код
total = 0
for i in range(1000000):
total += i*i
print(f"Время, замеренное таймером: {t.interval:.4f} секунд")
Условный пример вывода; фактическое время зависит от окружения:
Блок кода выполнялся 0.1234 секунд Время, замеренное таймером: 0.1234 секунд
Здесь класс Timer в методе __enter__ сохраняет текущее время self.start и возвращает себя. В методе __exit__ вычисляет интервал времени и выводит его на экран. Также сохраняет интервал в self.interval, чтобы мы могли после блока with обратиться к этому значению (например, для дополнительной обработки). Мы решили не подавлять возможное исключение внутри блока (метод возвращает None), чтобы оно при необходимости было выброшено наружу.
Другой пример: менеджер контекста, который временно переопределяет текущее рабочее окружение (например, текущую директорию). В модуле os есть функция chdir для смены текущей директории. Можно написать контекстный менеджер, который при входе запоминает текущую директорию и переходит в новую, а при выходе возвращается назад.
import os
class ChangeDir:
def __init__(self, new_path):
self.new_path = new_path
self.old_path = None
def __enter__(self):
self.old_path = os.getcwd() # запоминаем текущую директорию
os.chdir(self.new_path) # переходим в новую
return None # можем не возвращать ничего (или вернуть self, если нужно)
def __exit__(self, exc_type, exc_val, exc_tb):
os.chdir(self.old_path) # возвращаемся в старую директорию
# Исключения не обрабатываем специально, возвращаем False
Использование:
print("Before:", os.getcwd())
with ChangeDir("/tmp"):
# внутри блока текущая директория изменена на /tmp
print("Inside:", os.getcwd())
# после выхода директория восстановлена
print("After:", os.getcwd())
После успешного входа этот менеджер пытается восстановить исходную директорию и при исключении в блоке. Восстановление может завершиться ошибкой, например если исходная директория удалена. Если ошибка возникла в __enter__, Python не вызывает __exit__: частично захваченные ресурсы нужно освобождать в самом __enter__.
os.chdir меняет рабочую директорию всего процесса. Этот пример предназначен для последовательного кода; он не подходит для параллельных потоков или корутин, зависящих от рабочей директории. Путь /tmp — пример для Unix: используйте существующую временную папку своей системы.
FAQ
Задавайте вопросы в комментариях — мы будем расширять раздел.
-
Зачем
__enter__возвращаетself(или другой объект)?
Возвращённое значение привязывается к имени послеas ...в конструкцииwith. Если нужен доступ к данным менеджера внутри блока, обычно возвращаютself(with Timer() as t:). Если ничего не нужно — можно вернутьNone. -
Что будет, если в
__exit__вернутьTrue?
Исключение, возникшее в блокеwith, будет подавлено и не «уйдёт» выше по стеку. ВозвратFalse(илиNone) оставляет исключение активным. -
Можно ли использовать
printв__exit__вместо логирования?
Технически можно, но для реального кода лучше применитьlogging— так сообщения можно фильтровать и перенаправлять. -
Что произойдёт, если в
__exit__само возникнет новое исключение?
Наружу выйдет новое исключение, а исходное обычно останется в его__context__и будет видно в цепочке traceback.contextlib.ExitStackсам по себе не подавляет ошибки очистки. Политику обработки таких ошибок нужно задать явно. -
Нужно ли явно вызывать
Timer.__exit__внеwith?
После успешного__enter__конструкцияwithвызовет__exit__автоматически. Ручной вызов допустим (например, при тестировании), но редко нужен. -
Как сделать менеджер, который только подавляет исключения определённого типа?
Проверяйтеexc_typeи возвращайтеTrueтолько для нужных исключений:def __exit__(self, exc_type, exc_val, tb): return exc_type is FileNotFoundError -
Можно ли написать менеджер без класса?
Да, с помощью@contextlib.contextmanager:Код доimport time from contextlib import contextmanager @contextmanager def timer(): start = time.perf_counter() try: yield finally: print(time.perf_counter() - start)yieldвыполняется при входе. Исключение из блока возобновляет генератор в точкеyield; очистка вfinallyвыполняется и при таком выходе. -
Будет ли
ChangeDirбезопасен при вложенных вызовах?
Если одновременно используется несколько экземпляров («матрёшка»), каждый запоминает своюold_path, поэтому восстановление происходит корректно в последовательном коде, если исходные директории доступны. Повторный вложенный вход в тот же экземпляр перезапишетold_path. Изменение рабочей директории действует на весь процесс, поэтому параллельное использование небезопасно.
Задача 1
Временная подмена атрибута
Реализуйте класс-контекстный менеджер temporary_attribute(obj, name, value). При входе он временно устанавливает obj.<name> = value и возвращает obj. При выходе восстановите прежнее значение; если атрибута до входа не существовало, удалите его. Восстановление обязательно и после исключения.
Пример
config = type("Config", (), {"mode": "prod"})()
with temporary_attribute(config, "mode", "test") as current:
current.mode # "test"
config.mode # "prod"
Менеджер не подавляет исключения из блока.
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
