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

В прошлый раз я много раз обратил внимание на то, что файлы надо закрывать после того как завершена работа. Но в реальности в большой программе отслеживать закрыт ли файл становится достаточно проблематично. Бывает вы открываете файл в одной части программы, потом передаете его в качестве параметра в функцию, где он должен быть обработан построчно, а строки могут быть переданы еще куда-то и в результате можно просто забыть закрыть файл. И как следствие либо получить ситуацию когда у вас открыто множество файлов которые уже не нужны, либо перестараться и закрыть файл до того как из него будут прочитаны все данные и получть сообщение об ошибке.

Эту ситуацию можно было бы избежать если обрабатывать файл не построчно, а сразу загрузить весь объем в оперативную память с помощью метода read(). Но этот метод будет работать до первого большого файла.

Чтобы избежать подобные ситуации я хочу вам предложить способ с помощью менеджера контекста with. Эта тема определенно относится к более продвинутому курсу языка, но лучше сразу сформировать привычку которая в последствии окупится много раз.

Работа с файлами в Python достаточно прямолинейна с точки зрения необходимых действий. Вы открываете файл функцией open она возвращает специальный файловый объект, с которым осуществляются операции ввода-вывода и потом обязательно вызывается метод close() для того чтобы закрыть работу с потоком. Как оказалось это очень распространенный шаблон работы, например с файлами или сетевыми соединениями, создается объект который нужен непродолжительное время, но у которого обязательно нужно вызвать методы закрытия. Именно так и работает менеджер контекста with.

Синтаксис конструкции выглядит так:

with <expr1> as <name1>, [expr2 as name2, ...]:
    <block contents that can use name>

Выражение expr1 возвращает объект который можно записать в переменную name1, когда блок завершит работу, то у перемпенной name1 вызовется специальный метод закрытия, если это файл то метод вызовет метод close(). Если необходимо в одном контексте создать сразу несколько переменных, например открыть несколько файлов, то их можно указывать через запятую.

Вот измененный код программы которая выводит содержимое текста на экран построчно Файл reader2py:

1
2
3
with open("turing_paper_1936.txt", "rt") as paper:
    for line in paper:
        print(line[:-1])

Больше нет необходимости добавлять paper.close() после тела цикла for. И самое главное, что с таким кодом гораздо легче не совершать лишних ошибок.

Под капотом with

Менеджер контекста внутри работает следующим образом: после того выражение expr1 создает переменную, то вызывается магический метод __enter__, после завершения блока вызывается метод __exit__. Файловый объект имеет эти методы специально для того чтобы работать с with. Если вам интересно посмотреть на реализацию подобного механизма самостоятельно, то можете взять в качестве примера такой код:

class context:
    def __enter__(self):
        # подготовка объекта
        return self  # то что тут вернется будет присвоенно переменной стоящей после as
    def __exit__(self, type, value, traceback):
        # код отвечающий за выход из контекста, например тут может находится вызов self.close()
        # обработка ошибок если они возникли
        pass

with context() as obj:
     # работа с объектом obj

В действительности with нечто среднее между try/except и циклом тело которого выполняется только один раз.

Еще один пример копирует содержимое файла turing_paper_1936.txt в файл turing_paper_copy.txt.

Файл new_copy.py:

1
2
3
4
5
with open("turing_paper_1936.txt", "rt") as in_file, open(
    "turing_copy.txt", "wt"
) as out_file:
    for line in in_file:
        out_file.write(line)

С менеджером контекста with гораздо проще. Не надо следить за тем чтобы закрыть оба файла и конечный код содержит гораздо меньше лишних строк кода.

Ссылки