Менеджеры контекста в Python  -  выходим за пределы

Введение

В Python при работе с файлами наиболее распространённой функция open(), создающая объект типа файл, который в зависимости от ситуации позволяет читать или записывать данные. Мы используем функцию open() почти всегда с оператором with, согласно официальному справочнику и онлайн руководствам. Основная форма показана ниже.

>>> with open('hello.txt', 'w') as file:
...     file.write("Hello World!")

Запустив код, видим, что файл с именем hello.txt был создан в рабочей директории. Чтобы проверить, что строка Hello World! записана в файл, откроем его: 

>>> with open('hello.txt', 'r') as file:
...     text = file.read()
...     print('Text in the file:', text)
... 
Text in the file: Hello World!

Мы можем прочитать файл, открыв его с помощью функции open(), задав режим чтения (r ).

Автоматическое закрытие файла

Возможно, многие знают, почему для открытия файла здесь мы применяем оператор with. Те, кто не знает, просмотрите сперва следующий код:

>>> with open('hello.txt', 'a') as file:
...     file.write('\nHello Python!')
... 
... closed = file.closed
... print("Is the file closed?", closed)
... 
Is the file closed? True

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

И что же в этом такого? Рассмотрим следующий тривиальный пример:

>>> # Добавляем новые данные
... file0 = open("hello.txt", "a")
... file0.write("\nHello World Again!")
... 
... # Снова читаем файл
... file1 = open("hello.txt")
... print(file1.read())
... 
Hello World!
Hello Python!

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

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

В более широком смысле оператор with для открытия файла  —  это пример использования менеджера контекста.

Что такое менеджер контекста? Это объект Python, который выполняет за вас рутинную работу, когда вы используете определённые ресурсы. В частности, менеджер контекста задаёт временный контекст и ликвидирует его после выполнения операций.

Если говорить об операции открытия файла, работу менеджера контекста можно продемонстрировать с помощью операторов try, except иfinally. Рассмотрим следующий псевдокод для возможной реализации оператора with:

try:
    # открытие файл и создание объекта
    file = open('hello.txt')
    # выполнение конкретных операций
except Exception:
    # обработка любых возможных исключений
    raise 
finally:
    # публикация файла
    file.close()

Менеджер контекста открывает файл и создаёт объект, с которым дальше будет производиться работа. Когда мы завершим операцию и любые исключения, выброшенные в процессе выполнения операции, менеджер контекста закроет файл. Поскольку файлы являются совместно используемыми ресурсами и находятся в вашей ответственности (задачу управления ими помогают решать менеджеры контекста), критически важно, чтобы вы освободили их после того, как выполните операции, чтобы другие процессы получили к ним доступ.

Использование с обработкой сообщений

Как мы уже обсудили, оператор with лучше всего применять в ситуациях, когда нам необходимо работать с чем-то коллективно используемым. Один из ярких примеров  —  работа с данными в многопоточных проектах. Вам наверняка известно, к какому беспорядку приводит ситуация, когда несколько потоков получают доступ к одному и тому же массиву данных: одна операция добавляет данные, в то время как другая читает старые данные в другой файловой операции.

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

from threading import Lock

# С оператором with
with Lock():
    # do your operations here
    pass

# Без оператора with
lock = Lock()
lock.acquire()
try:
    # выполнение операций
except Exception:
    # обработка исключений
finally:
    lock.release()

Как видим, оператор with может кардинально улучшить осмысленность вашего кода. Что ещё более важно, блокировка снимается автоматически, когда операция завершается с оператором with. Без использования менеджера контекста, то есть оператора with, нам придётся управлять этими ресурсами вручную и очень осторожно. Если мы забудем снять блокировку, наша программа столкнётся с неожиданными проблемами.

Протокол менеджера контекста

Создавать менеджеры контекста можно, чтобы самостоятельно управлять некоторыми ресурсами. Одним из способов создания является реализация методов для протокола менеджера контекста. Можете представить это себе как утиную типизацию  —  мы просто определим магические методы __enter__ и __exit__ без формального согласования протокола или реализации интерфейса, как это можно сделать и в других языках программирования. Следующий код демонстрирует эту концепцию:

>>> class ContextManagerExample:
...     def __init__(self):
...         print("Context Manager Created")
... 
...     def __enter__(self):
...         print("Begin Context Management")
... 
...     def __exit__(self, exc_type, exc_val, exc_tb):
...         print("End Context Management")
... 
... 
... with ContextManagerExample():
...     print("Run operations in the with statement")
... 
Context Manager Created
Begin Context Management
Run operations in the with statement
End Context Management

Как показано выше, мы просто определили класс, в котором реализованы методы __enter__ и __exit__, способные управлять контекстом за нас. С синтаксической точки зрения, мы можем использовать этот класс в операторе with, как в строке 12. Выведенный текст показывает нам порядок, в котором эти операции хорошо координируются. В частности, созданный экземпляр (строка 15) вызовет метод __enter__ (строка 16) для запуска контекста, затем мы сами выполняем операции (строка 17), и, наконец, менеджер контекста выйдет из управления, вызвав метод __exit__.

Модуль contextlib

Вы обнаружите, что самостоятельная реализация специальных методов __enter__ и __exit__ для создания менеджера контекста может оказаться утомительной. С модулем contextlib в стандартной библиотеке Python намного проще управлять контекстом. Полный обзор этого модуля выходит за рамки данной статьи, я просто расскажу о конкретном методе для создания менеджера контекста. Но сперва давайте немного вернёмся назад, потому что здесь уместно упомянуть декораторы.

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

>>> def invocation_log(func):
...     def inner_func(*args, **kwargs):
...         print(f'Before Calling {func.__name__}')
...         func(*args, **kwargs)
...         print(f'After Calling {func.__name__}')
... 
...     return inner_func
... 
... 
... @invocation_log
... def say_hello(name):
...     print(f"Hello, {name}!")
... 
... 
... say_hello("Python")
... 
Before Calling say_hello
Hello, Python!
After Calling say_hello

Для декораторов вам просто нужно создать функцию, которая принимает другую функцию в качестве входных данных. Декорация  —  это операция, определённая в функции-декораторе. В данном случае мы просто будем делать записи до и после вызова функции. Чтобы использовать декоратор, напишем имя функции с префиксом @ . Можно сказать, что вызов декорированной функции (строка 15) успешно привёл к дополнительному логированию до и после вызова функции.

Теперь, разобравшись с декораторами, рассмотрим пример использования модуля contextlib, который поможет нам с управлением контекстом в следующем фрагменте кода:

>>> from contextlib import contextmanager
... 
... @contextmanager
... def context_manager_example():
...     print("Create the context")
...     yield
...     print("Destroy the context")
... 
... 
... with context_manager_example():
...     print("Run operations with the context")
... 
Create the context
Run operations with the context
Destroy the context

Используем функцию-декоратор contextmanager для декорирования функции context_manager_example. В теле функции вы можете заметить нечто необычное  —  ключевое слово yield. Вы уже должны были встретиться с этим словом, когда изучали генераторы  —  итераторы, отображающие элементы, когда их об этом просят (так называемое ленивое вычисление). В этих случаях “yield” означает “продукт”.

Однако в нашем случае это слово означает “уступать”. В частности как только менеджер контекста (декорированная функция context_manager_example) завершает настройку, она уступает выполнение коду с оператором with. После завершения операции контроль возвращается к функции. Важно, что yield в Python специально обрабатывается, поэтому он запускается в том месте, где был запущен. Вот почему функция print, следующая за ключевым словом yield, вызывается только один раз сразу после завершения операций в операторе with.

Выводы

В этой статье мы рассмотрели концепцию менеджеров контекста на примере операции с файлами, включающей оператор with. Мы усвоили, что именно менеджер контекста помог нам выполнить вспомогательную работу, закрыв файл.

В более широком смысле менеджеры контекста полезны для управления ресурсами конкретной программы или других программ на компьютере, предназначенных для совместного использования. Менеджеры контекста помогают ответственно управлять получением и освобождением этих совместных ресурсов. Мы также рассмотрели, как можно переопределять методы __enter__ и __exit__ для создания собственных классов менеджера контекста. Кроме того, рассмотрели альтернативный метод применения модуля contextlib для создания менеджеров контекста с использованием декораторов.

Однако мы осветили не все вопросы. Например, сигнатура функции метода __exit__ имеет другие параметры, которые мы не реализовали, например, обработку исключений. Полная реализация этих параметров должна оцениваться в индивидуальном порядке.

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Yong Cui, Ph.D.: Context Managers in Python — Go Beyond “with open() as file”