На этом шаге мы рассмотрим особенности реализации этой инструкции в проектах.
Нужно сказать, что в функции open() или классе threading.Lock нет ничего особенного или чудесного, равно как и в том, что они могут применяться вместе с инструкцией with. Ту же самую функциональность можно обеспечить в собственных классах и функциях путем реализации так называемых менеджеров контекста (context managers).
Что такое менеджер контекста? Это простой "протокол" (или интерфейс), который ваш объект должен соблюдать для того, чтобы поддерживать инструкцию with. В сущности, если вы хотите, чтобы объект функционировал как менеджер контекста, от вас требуется только одно - добавить в него методы __enter__() и __exit__(). Python будет вызывать эти два метода в соответствующих случаях в цикле управления ресурсом.
Давайте посмотрим, как это выглядит на практике. Вот пример простой реализации контекстного менеджера open():
class ManagedFile: def __init__(self, name): self.name = name def __enter__(self): self.file = open(self.name, 'w') return self.file def __exit__(self, exc_type, exc_val, exc_tb): if self.file: self.file.close()
Наш класс ManagedFile подчиняется протоколу менеджера контекста и теперь поддерживает инструкцию with точно так же, как и первоначальный пример с функцией open():
>>> with ManagedFile('hello.txt') as f: f.write('Привет, мир!') f.write('А теперь, пока!')
Python вызывает __enter()__, когда поток исполнения входит в контекст инструкции with и наступает момент получения ресурса. Когда поток исполнения снова покидает контекст, Python вызывает __exit__(), чтобы высвободить этот ресурс.
Написание менеджера контекста на основе класса не является единственным способом поддержки инструкции with в Python. Служебный модуль contextlib стандартной библиотеки обеспечивает еще несколько абстракций, надстроенных поверх базового протокола менеджера контекста. Он может слегка облегчить вашу жизнь, если ваши варианты применения совпадают с тем, что предлагается модулем contextlib.
Например, вы можете применить декоратор contextlib.contextmanager, чтобы определить для ресурса фабричную функцию на основе генератора, которая затем будет автоматически поддерживать инструкцию with. Вот как выглядит пример нашего контекстного менеджера ManagedFile, переписанный в соответствии с этим приемом:
from contextlib import contextmanager @contextmanager def managed_file(name): try: f = open(name, 'w') yield f finally: f.close() >>> with managed_file('hello.txt') as f: f.write('Привет, мир!') f.write('А теперь, пока!')
В данном случае managed_file() является генератором, который сначала получает ресурс. После этого он временно приостанавливает собственное исполнение и передает ресурс инструкцией yield, чтобы его использовал источник вызова. Когда источник вызова покидает контекст with, генератор продолжает выполняться до тех пор, пока не произойдут любые оставшиеся шаги очистки, после чего ресурс будет высвобожден и возвращен системе.
Реализации на основе класса и на основе генератора по своей сути эквивалентны. Вы можете предпочесть тот или иной вариант в зависимости от того, какой подход вы считаете более удобочитаемым.
Оборотной стороной реализации на основе @contextmanager может являться то, что такая реализация требует некоторого вникания в продвинутые понятия языка Python, такие как декораторы и генераторы.
Повторим еще раз: правильный выбор реализации здесь сводится к тому, с какой из них вы и ваша команда чувствуете себя комфортно и какую из них вы считаете наиболее удобочитаемой.
На следующем шаге мы рассмотрим написание красивых API с менеджерами контекста.