Pandora ResearchPandora
Research
RUEN
Python

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

Pandora ResearchPandora Research
3 июля 20266 мин чтения

В этой статье поговорим про контекстные менеджеры и ключевое слово with. Для чего это всё нужно?

Лучше всего начать с уже знакомого примера. Допустим, вам надо открыть файл и записать в него секретную или не очень информацию. Как бы вы это сделали, зная о принципах работы с файлами и про встроенную функцию open? Да примерно вот так:

Python
f = open("file.bin", "wt")
f.write("MYSECRETINFO")
f.close()

И это действительно будет работать. А теперь представим, что вы забыли закрыть файл и потеряли всю информацию в нём. Это не очень приятно — особенно если вы писали, например, собственную базу данных. Но дело даже не в том, что можно забыть закрыть файл: даже если вы человек-пунктуальность, лучше не испытывать судьбу. Вы же не станете чинить проводку самостоятельно, даже зная закон Ома? Вот и здесь так же: для работы с ресурсами лучше обратиться к специальным возможностям языка. Так вы точно не выстрелите себе в ногу, добившись при этом такого же результата.

Для работы с ресурсами лучше обратиться к специальным возможностям языка — так вы точно не выстрелите себе в ногу.

Что такое контекстный менеджер

Давайте договоримся о понятии «контекстный менеджер». Контекстный менеджер — определённая структура в языке Python (класс или генератор), основывающаяся на главном принципе: при его открытии и закрытии срабатывает заранее написанный программистом код.

Открытие происходит при входе в блок с помощью ключевого слова with. Закрытие происходит, когда блок заканчивается. Например, на входе — открывается файл, на выходе — закрывается.

Самый простой пример использования контекстного менеджера с ключевым словом with — работа с файлами:

Python
with open("file.bin", "wt") as f:  # открываем файл с помощью with
    f.write("abcdefg")

В этом примере мы открываем файл с помощью контекстного менеджера, который автоматически закроет его за нас. Почему бы просто не закрыть файл методом .close()? Дело в том, что на выходе из контекстного менеджера может понадобиться выполнить не один десяток операций, в том числе обращений к стороннему API или базе данных. Не станете же вы каждый раз писать один и тот же код? И даже если вы напишете функцию или метод, делающий это за вас, его снова надо не забыть вызвать, иначе всё будет напрасно. Гораздо проще один раз написать контекстный менеджер и вызывать его каждый раз, когда нужно, чтобы он выполнил всю грязную работу за нас.

Менеджер на основе класса

Пишутся контекстные менеджеры очень легко — по сути, это всего-то по-особенному устроенные классы. Чтобы написать контекстный менеджер, нужно помнить о нескольких вещах:

Нужно создать класс и написать в нём метод __enter__. Код в этом методе будет выполняться при входе в контекстный менеджер (при создании объекта с ключевым словом with).

Написать метод __exit__ — он выполнит помещённый в него код на выходе.

Добавить в этот метод три дополнительных аргумента помимо selfexc_type, exc_val, exc_tb. Зачем они нужны, расскажем чуть позже.

А сейчас давайте попробуем написать простенький измеритель времени исполнения нашего кода:

Python
from datetime import datetime
import time  # проверять действие измерителя будем с помощью библиотеки time


# вся суть этого измерителя заключается в том, что мы считаем разницу в секундах между открытием и закрытием контекстного менеджера
class Timer:
    def __init__(self):
        pass

    def __enter__(self):  # этот метод вызывается при запуске с помощью with. Если вы хотите вернуть какой-то объект, чтобы потом работать с ним в контекстном менеджере, как в примере с файлом, то просто верните этот объект через return
        self.start = datetime.utcnow()
        return None

    def __exit__(self, exc_type, exc_val, exc_tb):  # этот метод срабатывает при выходе из контекстного менеджера
        print(f"Time passed: {(datetime.utcnow() - self.start).total_seconds()}")


with Timer():
    time.sleep(2)  # засыпаем на 2 секунды

В консоль должно вывестись примерно следующее:

Terminal
Time passed: 2.00099

В дополнение к комментариям в коде скажем пару слов об аргументах метода __exit__. О каждом из них по порядку:

exc_type — тип исключения, из-за которого вышел контекстный менеджер. Если всё прошло успешно, значение этого аргумента будет None.

exc_val — сообщение в исключении. Аналогично: если всё прошло успешно, этот аргумент будет None.

exc_tb — объект трассировки от интерпретатора. Его лучше вообще не трогать, если вы не разработчик языка, но тем не менее он всегда ждёт вас здесь.

Менеджер на основе генератора

Python не был бы Python, если бы у него не было библиотек на любой крайний случай. В далёкой версии Python 2.6 появилась возможность создания контекстных менеджеров через генераторы. Давайте взглянем, как это можно сделать.

Но перед этим разъясним разницу в реализации. По сути, здесь разница только в том, что контекстный менеджер на генераторах — это функция (для особо привередливых — генератор). В ней до yield выполняется код, который мы могли бы записать в __enter__, если бы делали контекстный менеджер в виде класса, а после yield пишем код, который выполнился бы в __exit__. То есть до yield — всё, что произойдёт при входе, после — всё, что на выходе. Вот и вся разница; как видите, она невелика.

Давайте теперь посмотрим код:

Python
from datetime import datetime
import time

from contextlib import contextmanager  # импортируем нужный нам декоратор


@contextmanager  # оборачиваем функцию в декоратор contextmanager
def timer():
    start = datetime.utcnow()
    yield  # если вам нужно что-то вернуть через контекстный менеджер, просто вставьте этот объект сюда.
    print(f"Time passed: {(datetime.utcnow() - start).total_seconds()}")


with timer():
    time.sleep(2)

Результат работы такого контекстного менеджера на основе генераторов абсолютно такой же:

Terminal
Time passed: 2.001097

Итоги

Подведём итоги. Мы узнали, зачем нужен контекстный менеджер, и на практике убедились, насколько он удобен.

Контекстный менеджер вызывается с помощью ключевого слова with. Он может возвращать или не возвращать объект для работы. Например, если контекстный менеджер подразумевает работу с каким-либо объектом, нужно добавить в запись as var, где var — имя переменной в данном контексте.

Мы научились строить собственные контекстные менеджеры на основе классов. Для этого пишем класс со специальными методами __enter__(self, ...) и __exit__(self, exc_type, exc_val, exc_tb). __enter__ вызывается при входе в контекстный менеджер, __exit__ — при выходе (не забудьте добавить необходимые аргументы).

Также научились строить контекстные менеджеры на основе генераторов и декоратора contextmanager из библиотеки contextlib.

Не бойтесь применять их в работе — экспериментируйте!

Python

Есть задача для нашей команды?

Расскажите о проекте — оценим и предложим решение в течение одного рабочего дня.

Обсудить проект