Контекстный менеджер в Python
В этой статье вы узнаете, что такое контекстный менеджер и как его эффективно использовать в Python.
Менеджер контекста — это объект, определяющий контекст выполнения в операторе with.
Давайте начнем с простого примера, чтобы понять концепцию менеджера контекста.
Предположим, что у yас есть файл data.txt, в котором содержится целое число 100.
Теперь напишем программу, которая читает файл data.txt, преобразует его содержимое в число и выводит результат на стандартный вывод:
f = open('data.txt')
data = f.readlines()
# преобразует содержимое в целое число и выводит его
print(int(data[0]))
f.close()
Код прост и понятен.
Однако что если в файле data.txt будут содержаться данные, которые не нельзя преобразовать в целое число? В таком случае возникнет исключение ValueError.
Например, если в файл data.txt записать строку '100' вместо числа 100, вы получите следующую ошибку:
ValueError: invalid literal for int() with base 10: "'100'"
Из-за этого исключения Python может не закрыть файл должным образом.
Чтобы исправить такое поведение, можно воспользоваться оператором try...except...finally
:
try:
f = open('data.txt')
data = f.readlines()
# преобразует содержимое в целое число и выводит его
print(int(data[0]))
except ValueError as error:
print(error)
finally:
f.close()
Поскольку код в блоке finally
всегда выполняется, программа всегда будет правильно закрывать файл.
Это решение работает как надо, но оно слишком многословное.
Поэтому в Python есть способ автоматически закрыть файл после завершения его обработки — и он менее многословный.
Здесь в игру вступают менеджеры контекста.
Ниже показано, как использовать менеджер контекста для обработки файла data.txt:
with open('data.txt') as f:
data = f.readlines()
print(int(data[0])
В этом примере мы используем функцию open()
с оператором with
. После блока with
Python автоматически закроется.
Оператор with
Синтаксис
with context as ctx:
# используем объект
# очищаем контекст
Как это работает
- Когда Python встречает оператор
with
, он создает новый контекст. При желании контекст может возвращать объект. - После блока
with
Python автоматически очищает контекст. - Область видимости
ctx
имеет ту же область видимости, что и операторwith
. Это означает, что вы можете обращаться кctx
как внутри оператораwith
, так и после него.
Ниже показано, как получить доступ к переменной f
после оператора with
:
with open('data.txt') as f:
data = f.readlines()
print(int(data[0]))
print(f.closed) # Вывод: True
Протокол контекстного менеджера
Контекстные менеджеры Python работают на основе протокола контекстного менеджера.
Протокол менеджера контекста включает следующие методы:
__enter__()
— устанавливает контекст и, по желанию, возвращать некоторый объект.__exit__()
— очищает объект.
Если вы хотите, чтобы класс поддерживал протокол контекстного менеджера, вам необходимо реализовать эти два метода.
Предположим, что у нас есть некий классContextManager
, поддерживающий протокол контекстного менеджера.
Вот, как можно использовать этот класс:
with ContextManager() as ctx:
# что-то делаем
# закончили с контекстом
Когда вы используете класс ContextManager
с оператором with
, Python неявно создает экземпляр класса — instance
— и автоматически вызывает метод __enter__()
на этом экземпляре.
Метод __enter__()
может по желанию возвращать объект. Если это так, Python присваивает возвращаемый объект ctx
.
Обратите внимание, что ctx
ссылается на объект, возвращаемый методом __enter__()
. Он не ссылается на экземпляр класса ContextManager
.
Если внутри блока with
или после блока with
возникает исключение, Python вызывает метод __exit__()
на объекте экземпляра.
Функционально оператор with
эквивалентен конструкции try...finally
:
instance = ContextManager()
ctx = instance.__enter__()
try:
# что-то делаем с txt
finally:
# закончили с контекстом
instance.__exit__()
Метод __enter__()
В методе __enter__()
можно выполнить необходимые действия по настройке контекста.
При необходимости вы можете вернуть объект из метода __enter__()
.
Метод __exit__()
Python всегда выполняет метод __exit__()
, даже если в блоке with
возникает исключение.
Метод __exit__()
принимает три аргумента: тип исключения, значение исключения и объект трассировки. Все эти аргументы будут равны None
, если исключение не произошло.
def __exit__(self, ex_type, ex_value, ex_traceback):
...
Метод __exit__()
возвращает логическое значение: True
или False
.
Если возвращаемое значение равно True
, Python заглушит исключение.
Как можно использовать контекстный менеджер
В этой статье мы уже выяснили, что контекстный менеджер можно использовать для автоматического открытия и закрытия файлов.
Давайте разберемся, в каких случаях еще можно использовать контекстный менеджер. Вот некоторые из них:
1) Закрытие — открытие
Если вы хотите открывать и закрывать ресурс автоматически, вы можете использовать контекстный менеджер.
Например, вы можете открыть сокет и закрыть его с помощью контекстного менеджера.
2) Заблокировать — разблокировать
Контекстные менеджеры помогут вам более эффективно управлять блокировками объектов.
3) Запустить — остановить
Контекстные менеджеры помогут вам работать со сценариями, требующими запуска и остановки.
Например, можно использовать контекстный менеджер для запуска таймера и его автоматической остановки.
4) Изменить — сбросить
Контекстные менеджеры могут работать со сценариями изменения и сброса.
Например, вашему приложению необходимо подключиться к нескольким источникам данных. И у него есть соединение по умолчанию.
Вот алгоритм для подключения к другому источнику данных:
- Используйте контекстный менеджер для изменения соединения по умолчанию на новое.
- Работайте с новым соединением.
- После завершения работы с новым соединением верните его обратно к соединению по умолчанию.
Создание протокола контекстного менеджера
Ниже показана простая реализация функции open()
с использованием протокола контекстного менеджера:
class File:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
def __enter__(self):
print(f'Opening the file {self.filename}.')
self.__file = open(self.filename, self.mode)
return self.__file
def __exit__(self, exc_type, exc_value, exc_traceback):
print(f'Closing the file {self.filename}.')
if not self.__file.closed:
self.__file.close()
return False
with File('data.txt', 'r') as f:
print(int(next(f)))
Как это работает
- Инициализируем имя файла и режим в методе
__init__()
. - Открываем файл в методе
__enter__()
и возвращаем объект файла. - Закрываем файл, если он открыт, в методе
__exit__()
.
Реализации шаблона запуска и остановки с помощью контекстного менеджера
Давайте создадим класс Timer
, который поддерживает протокол контекстного менеджера:
from time import perf_counter
class Timer:
def __init__(self):
self.elapsed = 0
def __enter__(self):
self.start = perf_counter()
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
self.stop = perf_counter()
self.elapsed = self.stop - self.start
return False
Как это работает
- Импортируем
perf_counter
из модуляtime
. - Запускаем таймер в методе
__enter__()
. - Останавливаем таймер в методе
__exit__()
и верните прошедшее время.
Теперь вы можете использовать класс Timer
для измерения времени, необходимого для вычисления числа Фибоначчи, равного 1000, один миллион раз:
def fibonacci(n):
f1 = 1
f2 = 1
for i in range(n-1):
f1, f2 = f2, f1 + f2
return f1
with Timer() as timer:
for _ in range(1, 1000000):
fibonacci(1000)
print(timer.elapsed)
Что нужно запомнить
- Используйте менеджеры контекстов для определения контекстов времени выполнения при выполнении в операторе
with
. - Для поддержки протокола менеджера контекста нужно реализовать методы
__enter__()
и__exit__()
.