С
менеджером контекста мы с вами уже сталкивались, когда рассматривали работу с
файлами. Дело в том, что когда открываем файловый поток с помощью функции open(), то в конце работы с ним, его желательно закрыть с помощью
метода close(). Если реализовать эту логику через конструкцию try/except/finally, то получим примерно вот такой текст программы:
fp = None
try:
fp = open("myfile.txt")
for t in fp:
print(t)
except Exception as e:
print(e)
finally:
if fp is not None:
fp.close()
Благодаря
блоку finally мы гарантированно закрываем
файл, даже если в блоке try
возникло какое-либо исключение. Но, если воспользоваться файловым менеджером
контекста, то программа принимает такой вид:
try:
with open("myfile.txt") as fp:
for t in fp:
print(t)
except Exception as e:
print(e)
Видите,
она фактически отличается от первой тем, что не реализует блок finally. Но почему? Нам же нужно закрыть файл после работы с ним? Да, и
файловый менеджер контекста делает это автоматически вне зависимости от
возникновения возможных исключений. Как же в деталях это происходит? Для этого
мы сейчас посмотрим как вообще создаются свои собственные менеджеры контекстов.
Итак,
менеджер контекста – это класс, в котором реализованы два специальных метода:
__enter__() и __exit__()
Когда
происходит создание менеджера контекста с помощью оператора with, то автоматически вызывается метод класса __enter__. А когда менеджер контекста завершает свою работу (программа
внутри него выполнилась или произошло исключение), то вызывается метод __exit__. Как видите, все предельно просто. И, как вы догадались, в
файловом менеджере происходит закрытие файлового потока именно в методе __exit__.
Далее,
общий синтаксис вызова менеджера, следующий:
with
<менеджер контекста> as
<переменная>:
список конструкций языка Python
Здесь
«переменная» - это ссылка на экземпляр менеджера контекста, через которую, мы
потом с ним можем работать. При необходимости ее можно опустить и записать все
вот в таком виде:
with
<менеджер контекста>:
список конструкций языка Python
Но
тогда мы не сможем обратиться к объекту менеджера контекста.
Давайте
создадим свой класс менеджера, который бы контролировал работу при изменении
вектора: если программа в теле менеджера приводит к исключению (ошибке), то
вектор должен оставаться прежним (без изменений), иначе, мы бы получали
результат его обработки:
v1 = [1,2,3]
v2 = [1,2]
with DefenerVector(v1) as dv:
for i in range(len(dv)):
dv[i] += v2[i]
print(v1)
В
этом случае менеджер DefenderVector
может выглядеть так:
class DefenerVector:
def __init__(self, v):
self.__v = v
def __enter__(self):
self.__temp = self.__v[:] # делаем копию вектора v
return self.__temp
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
self.__v[:] = self.__temp
return False
Мы
здесь в приватном свойстве сохраняем ссылку на вектор, который следует
«защитить». Далее, в методе enter
создаем копию этого вектора и возвращаем его. То есть, переменная dv будет ссылаться на эту копию
и обработка внутри менеджера будет происходить с элементами этой копии, а не
исходным вектором. Затем, в методе exit мы
проверяем: если исключений не произошло, то заменяем все элементы вектора __v на преобразованные __temp. В
результате, при выходе из менеджера, мы получим измененный вектор v1. Если же было какое-либо исключение, то копирование выполняться
не будет и у нас останется прежний вектор v1.
Метод
exit у нас возвращает значение False, что означает обработку исключения (если оно произошло)
вышестоящим блоком. Обычно именно так и делают, чтобы не скрывать возможные
ошибки и в обработчике верхнего уровня реагировать должным образом на ошибочные
ситуации. Кстати, оператор return
можно вовсе опустить, тогда метод exit возвратит None, а
оно интерпретируется как False. Так
что, часто его не пишут.
Давайте
для примера возвратим значение True и
смотрите, при возникновении исключения, оно было перехвачено менеджером и далее
уже не распространялось. Снова вернем False,
запустим и теперь видим это исключение снова.
Вложенные менеджеры контекстов
При
необходимости, менеджеры контекстов можно вкладывать друг в друга. Например, при работе с файлами, можно выполнить такое вложение:
try:
with open("myfile.txt") as fin:
with open("out.txt", "w") as fout:
for line in fin:
fout.write(line)
except Exception as e:
print(e)
Задания для самоподготовки
1.
Создайте функтор для определения порядка сортировки списка p, состоящий из объектов класса Person:
class Person:
def __init__(self, surname, forename, old):
self.forename = forename
self.surname = surname
self.old = old
p = [Person("Иванов", "Иван", 20),
Person("Петров", "Степан", 21),
Person("Сидоров", "Альберт", 25)]
То
есть, вызывая функтор (пусть он называется SortKey) с
названием поля SortKey("surname"), сортировка выполнялась бы по этому
свойству. Если указать сразу два значения: SortKey("surname",
"forename"), то сортировка делалась бы по фамилии, но при их
равенстве – по имени.
(Подсказка:
используйте метод sort списка
p и его именованный параметр key).
2. Создайте менеджер контекста для
безопасной обработки элементов словаря. В случае возникновения исключения
словарь должен оставаться без изменений. Иначе (при успешной работе) он
сохранял бы все изменения.