Менеджеры контекстов. Оператор with

Курс по Python ООП: https://stepik.org/a/116336

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

Работает все очевидным образом. Сначала завершается (отрабатывает) вложенный менеджер, а затем, внешний (первый). Во всем остальном логика работы такого вложения сохраняется.

Думаю, теперь вы лучше узнали, что такое менеджеры контекста и зачем они нужны, а также сможете создавать свои менеджеры, если в этом возникнет необходимость.

Курс по Python ООП: https://stepik.org/a/116336

Видео по теме