Менеджеры контекста

С менеджером контекста мы с вами уже сталкивались, когда рассматривали работу с файлами. Дело в том, что когда открываем файловый поток с помощью функции 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. Создайте менеджер контекста для безопасной обработки элементов словаря. В случае возникновения исключения словарь должен оставаться без изменений. Иначе (при успешной работе) он сохранял бы все изменения.