На этом шаге мы рассмотрим алгоритм создания таких объектов.
Вы хотите заставить ваши объекты поддерживать протокол менеджера контекста (инструкцию 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. Потокобезопасную версию этого рецепта мы приведем позднее.
На следующем шаге мы рассмотрим экономию памяти при создании большого количества экземпляров.