Инструкция контекстного менеджера

Рассмотрим на примере чтения файла:
file = None
try:
   file = open('file.txt')
   text = file.read()
finally:
   if file is not None:
       file.close()
В этом примере мы видим что инструкции работы с файлом обернуты инструкциями создания и уничтожения дескриптора файла, они как бы выполняются контексте существования дескриптора файла.

В процессе программирования возникают ситуации когда некоторый блок инструкций нужно выполнить в рамках конкретного контекста, т.е в начале блока инструкций создать какой-то набор объектов, а в конце гарантированно уничтожить. Популярным примером этого является процесс чтения файла: перед чтением нужно открыть файл, а после закрыть. Данный протокол применим и ко многим другим объектам python: открытие и разрыв http-соединения, подключение и отключение к базе данных и др..

Инструкцию контекстного менеджера можно описать на примере инструкции обработки исключений, как в примере выше:

< создание объектов контекста >
try:
< блок инструкций с участием объектов контекста >
finally:
< уничтожение объектов контекста >

Но такая конструкция довольно громоздка, к тому же в python существует специальная инструкция создания контекстного менеджера with.
Протокол контекстного менеджера
Протокол реализует некоторый набор правил и требований к объекту, если объект этим требованиям соответствует, то можно сказать что объект реализует данный протокол. Python реализует ряд протоколов, одним из них является протокол контекстного менеджера.

Для того чтобы объект удовлетворял протоколу контекстного менеджера, он должен обладать методами:

  • __enter__() - выполняет создание объектов контекста.
  • __exit__() - выполняет удаление объектов контекста.
Составная инструкция with
Структура инструкции:

with < выражение > [ as < переменная контекста > ]:
< блок инструкций >

Алгоритм работы инструкции:

  1. Выполняется выражение в конструкции with ... as, при этом < выражение > должно быть менеджером контекста или воспроизводить его;
  2. Выполняется метод __enter__. Если конструкция with включает в себя слово as, то возвращаемое методом __enter__ значение записывается в < переменную контекста>;
  3. Выполняется < блок инструкций >
  4. Вызывается метод __exit__, причём неважно, выполнился < блок инструкций > или произошло исключение.
Набор вложенных инструкций можно записать в одну строку:

with < выражение > [as < переменная контекста >], (< выражение 2 > [ as < переменная контекста > ])*:
Рассмотрим типовые сценарии работы встроенных контекстных менеджеров

  1. Работа с файлами. В примерах ранее вы увидели что дескриптор файла реализует протокол контекстного менеджера.
  2. Работа с базой данных. Установку и разрыв соединения с базой данных можно также делать через менеджер контекста, т.к. объект psycopg2.connection реализует протокол контекстного менеджера.
3. Работа с http-сессиями
Создание собственного контекстного менеджера
В некоторых случаях требуется реализовать собственный контекстный менеджер, для каких-то специфических задач. Для этого требуется создать класс, реализующий методы протокола контекстного менеджера:
class ContextManager:

    def __enter__(self):
        #  создание контекста

    def __exit__(self, type, value, traceback):
        # уничтожение контекста
 
with ContextManager() as ctx:
    # набор инструкций
Создадим контекстный менеджер, который создает связь с базой данных SQLite, и закрывает её по окончанию работы. Вот простой пример:
import sqlite3

class SqliteConnection:

   def __init__(self, db_name):
       """ Конструктор """
       self.db_name = db_name

   def __enter__(self):
       """ Открываем подключение с базой данных """
       self.conn = sqlite3.connect(self.db_name)
       return self.conn

   def __exit__(self, exc_type, exc_val, exc_tb):
       """ Закрываем подключение """
       self.conn.close()
       if exc_val:
           raise


with SqliteConnection('test_db') as connection:
   cursor = connection.cursor()
   cursor.execute('SELECT * FROM auth_user')
   result = cursor.fetchall()
   print(result)
   connection.commit()
Модуль contextlib
Одновременно с добавлением инструкции with в стандартную библиотеку Python был добавлен модуль contextlib, предоставляющий широкий набор инструментов для работы с контекстными менеджерами. На данном этапе мы не будем углубляться в тему написания собственных контекстных менеджеров.