Шаг 132.
Python: сборник рецептов. Классы и объекты. Создание объектов, поддерживающих протокол менеджера контекста

    На этом шаге мы рассмотрим алгоритм создания таких объектов.

Задача

    Вы хотите заставить ваши объекты поддерживать протокол менеджера контекста (инструкцию with).

Решение

    Чтобы сделать объекты совместимыми с инструкцией with, вам нужно реализовать методы __enter__() и __exit__(). Например, рассмотрим следующий класс, который предоставляет сетевое соединение:

from socket import socket, AF_INET, SOCK_STREAM


class LazyConnection:
    def __init__(self, address, family=AF_INET, type=SOCK_STREAM):
        self.address = address
        self.family = AF_INET
        self.type = SOCK_STREAM
        self.sock = None

    def __enter__(self):
        if self.sock is not None:
            raise RuntimeError('Already connected')
        self.sock = socket(self.family, self.type)
        self.sock.connect(self.address)
        return self.sock

    def __exit__(self, exc_ty, exc_val, tb):
        self.sock.close()
        self.sock = None

    Ключевая возможность этого класса в том, что он представляет сетевое соединение, но изначально ничего не делает (т. е. не устанавливает соединение). Вместо этого соединение устаналивается и закрывается по запросу, с использованием инструкции with. Например:

from functools import partial


conn = LazyConnection(('www.python.org', 80))
# Соединение закрыто
with conn as s:
    # conn.__enter__() выполняется: соединение открыто
    s.send(b'GET /index.html HTTP/1.0\r\n')
    s.send(b'Host: www.python.org\r\n')
    s.send(b'\r\n')
    resp = b''.join(iter(partial(s.recv, 8192), b''))
    # conn.__exit__() выполняется: соединение закрыто
Архив с файлом можно взять здесь.

Обсуждение

    Главный принцип создания менеджера контекста в том, что вы пишете код, который будет окружен блоком инструкций согласно правилам использования инструкции with. Когда инструкция with впервые встречается интерпретатору, вызывается метод __enter__(). Возвращенное методом __enter__() значение (если оно есть) помещается в переменную, указанную с помощью квалификатора as. Затем выполняются инструкции в теле инструкции with. В конце вызывается метод __ exit__(), чтобы все очистить.

    Этот поток управления будет выполнен, несмотря на любые события в теле инструкции with - даже если будут возбуждены исключения. На самом деле три аргумента метода __exit__() содержат тип исключения, значение и трассировку для возбужденных исключений (если они имели место). Метод __exit__() может как-то использовать информацию об исключении либо проигнорировать ее, ничего не делая и возвращая None в качестве результата. Если __exit__() возвращает True, исключение исчезнет, как будто бы ничего и не произошло, и программа продолжит выполнение инструкций, следующих сразу за блоком with.

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

from socket import socket, AF_INET, SOCK_STREAM


class LazyConnection:
    def __init__(self, address, family=AF_INET, type=SOCK_STREAM):
        self.address = address
        self.family = AF_INET
        self.type = SOCK_STREAM
        self.connections = []

    def __enter__(self):
        sock = socket(self.family, self.type)
        sock.connect(self.address)
        self.connections.append(sock)
        return sock

    def __exit__(self, exc_ty, exc_val, tb):
        self.connections.pop().close()


# Пример использования
from functools import partial

conn = LazyConnection(('www.python.org', 80))
with conn as s1:
    ...
    with conn as s2:
        ...
        # s1 и s2 - независимые сокеты
Архив с файлом можно взять здесь.

    В этой второй версии класс LazyConnection служит своего рода фабрикой соединений. Внутри для хранения стека используется список. Когда бы ни был вызван метод __enter__(), он создает новое соединение и добавляет его на стек. Метод __exit__() просто выталкивает последнее соединение со стека и закрывает его. Это тонкий момент, но он позволяет создавать множество соединений за раз с помощью вложенных инструкций with, как и показано выше.

    Менеджеры контекста наиболее часто используются в программах, которым нужно управлять такими ресурсами, как файлы, сетевые соединения и блокировки. Ключевая особенность этих ресурсов в том, что они должны быть явно закрыты или освобождены, чтобы корректно работать. Например, если вы получаете блокировку(lock), то должны убедиться, что освобождаете ее, иначе вы рискуете получить зависание (deadlock). Путем реализации __enter__(), __exit__() и инструкции with избежать таких проблем намного проще, поскольку очищающий код в методе __exit__() будет гарантированно выполнен в любом случае.

    Альтернативную реализацию менеджеров контекста вы сможете найти в модуле contextmanager. Потокобезопасную версию этого рецепта мы приведем позднее.

    На следующем шаге мы рассмотрим экономию памяти при создании большого количества экземпляров.




Предыдущий шаг Содержание Следующий шаг