Одна из самых «непонятных» функций Python, которую используют почти все программисты, даже начинающие – это менеджеры контекста. Вы, наверное, видели их в форме операторов with, которые обычно впервые встречаются, когда вы изучаете открытие файлов в Python.
Хотя менеджеры контекста поначалу кажутся немного странными, когда мы действительно погружаемся в них, понимаем мотивацию и методы, стоящие за ними, мы получаем доступ к новому оружию в нашем арсенале программирования.
Управление ресурсами
Чтобы действительно понять, что такое диспетчер контекста и как мы можем его использовать, мы должны сначала исследовать мотивы, стоящие за ним – потребности, которые привели к этому «изобретению».
Основная цель менеджеров контекста в Python – это управление ресурсами. Когда программа хочет получить доступ к ресурсу на компьютере, она запрашивает об этом у ОС, а ОС, в свою очередь, предоставляет ей дескриптор этого ресурса. Некоторыми распространенными примерами таких ресурсов являются файлы и сетевые порты.
Важно понимать, что эти ресурсы имеют ограниченную доступность, например, сетевой порт может использоваться одним процессом за раз, а количество доступных портов ограничено. Поэтому всякий раз, когда мы открываем ресурс, мы должны помнить о его закрытии, чтобы ресурс был освобожден. Но, к сожалению, легче сказать, чем сделать.
Самый простой способ добиться правильного управления ресурсами – это вызвать функцию закрытия после того, как мы закончим с ресурсом. Например:
opened_file = open('readme.txt')
text = opened_file.read()
...
opened_file.close()
Здесь мы открываем файл с именем readme.txt, читаем файл и сохраняем его содержимое в виде строки текста, а затем, когда мы закончили с ним, закрываем файл, вызывая метод close() объекта open_file.
На первый взгляд это может показаться нормальным, но на самом деле это совсем не надежно. Если между открытием файла и закрытием файла произойдет что-то неожиданное, в результате чего программа не сможет выполнить строку, содержащую оператор close, произойдет утечка ресурсов. Эти неожиданные события – это то, что мы называем исключениями, обычно случаются, когда кто-то принудительно закрывает программу во время ее выполнения.
Теперь правильный способ справиться с этим – использовать обработку исключений с использованием блоков try … else. Взгляните на следующий пример:
try:
opened_file = open('readme.txt')
text = opened_file.read()
...
else:
opened_file.close()
Python всегда обеспечивает выполнение кода в блоке else, независимо от того, что может произойти. Таким образом программисты на других языках справляются с управлением ресурсами, но программисты получают специальный механизм, который позволяет им реализовать ту же функциональность без всяких шаблонов. Здесь в игру вступают контекстные менеджеры.
Реализация
Теперь, когда мы закончили с самой важной частью понимания менеджеров контекста, мы можем перейти к их реализации. В этом руководстве мы реализуем собственный класс File. Это полностью излишне, поскольку Python уже предоставляет это, но, тем не менее, это будет хорошее учебное упражнение, поскольку мы всегда сможем вернуться к классу File, который уже есть в стандартной библиотеке.
Стандартный и «низкоуровневый» способ реализации диспетчера контекста – это определение двух «волшебных» методов в классе, для которого вы хотите реализовать управление ресурсами, __enter__ и __exit__. Если вы начали заниматься объектно-ориентированным программированием на Python, вы наверняка уже сталкивались с методом __init__.
В любом случае, возвращаясь к теме, прежде чем мы начнем реализовывать эти два метода, мы должны понять их назначение. __enter__ – это метод, который вызывается, когда мы открываем ресурс, или, говоря несколько более технически, – когда мы «входим» в контекст выполнения. Оператор with привяжет возвращаемое значение этого метода к цели, указанной в предложении as оператора.
Давайте посмотрим на пример:
class FileManager:
def __init__(self, filename):
self.filename = filename
def __enter__(self):
self.opened_file = open(self.filename)
return self.opened_file
Как видите, метод __enter__ открывает ресурс – файл – и возвращает его. Когда мы используем этот FileManager в операторе with, этот метод будет вызван, и его возвращаемое значение будет привязано к целевой переменной, которую вы упомянули в предложении as. Я продемонстрировал в следующем фрагменте кода:
with FileManager('readme.txt') as file:
text = file.read()
Давайте разберемся по частям. Во-первых, экземпляр класса FileManager создается при его создании, передавая конструктору имя файла «readme.txt». Затем оператор with начинает работать с ним – он вызывает метод __enter__ этого объекта FileManager и присваивает возвращаемое значение файловой переменной, указанной в предложении as. Затем внутри блока with мы можем делать с открытым ресурсом все, что захотим.
Другая важная часть головоломки – это метод __exit__. Он содержит код очистки, который должен быть выполнен после того, как мы закончим работу с ресурсом, несмотря ни на что. Инструкции в этом методе будут аналогичны инструкциям в блоке else, который мы обсуждали ранее. Повторюсь, метод __exit__ содержит инструкции по правильному закрытию обработчика ресурсов, чтобы он был освобожден для дальнейшего использования другими программами в ОС.
Теперь давайте посмотрим, как мы могли бы написать этот метод:
class FileManager:
def __exit__(self. *exc):
self.opened_file.close()
Теперь, когда экземпляры этого класса будут использоваться в операторе with, этот метод __exit__ будет вызываться до того, как программа выйдет из блока with или до того, как программа остановится из-за некоторого исключения. Теперь давайте посмотрим на весь класс FileManager, чтобы получить полное представление.
class FileManager:
def __init__(self, filename):
self.filename = filename
def __enter__(self):
self.opened_file = open(self.filename)
return self.opened_file
def __exit__(self, *exc):
self.opened_file.close()
Мы только что определили действия открытия и очистки в соответствующих магических методах, а Python позаботится об управлении ресурсами, где бы этот класс ни использовался. Это подводит меня к следующей теме, различным способам использования классов диспетчера контекста, таких как этот класс FileManager.
Использование
Здесь особо нечего объяснять, поэтому вместо того, чтобы писать длинные абзацы, я приведу несколько фрагментов кода в этом разделе:
file = FileManager('readme.txt')
with file as managed_file:
text = managed_file.read()
print(text)
with FileManager('readme.txt') as managed_file:
text = managed_file.read()
print(text)
def open_file(filename):
file = FileManager(filename)
return file
with open_file('readme.txt') as managed_file:
text = managed_file.read()
print(text)
Как видите, главное помнить:
- Объект, переданный в оператор with, должен иметь методы __enter__ и __exit__.
- Метод __enter__ должен возвращать ресурс, который будет использоваться в блоке with.
Использование contextlib
Разработчики Python создали библиотеку с именем contextlib, содержащую утилиты для менеджеров контекста, как будто они недостаточно упростили проблему управления ресурсами. Я собираюсь кратко продемонстрировать только один из них.
from contextlib import contextmanager
@contextmanager
def open_file(filename):
opened_file = open(filename)
try:
yield opened_file
finally:
opened_file.close()
Как и в приведенном выше коде, мы можем просто определить функцию, которая возвращает защищенный ресурс в операторе try, закрывая его в последующем операторе finally. Другой способ понять это:
- Все содержимое, которое вы в противном случае поместили бы в метод __enter__, за исключением оператора return, находится здесь перед блоком try – в основном это инструкции по открытию ресурса.
- Вместо того, чтобы возвращать ресурс, вы передаете его внутри блока try.
- Содержимое метода __exit__ находится внутри соответствующего блока finally.
Когда у нас есть такая функция, мы можем украсить ее с помощью декоратора contextlib.contextmanager, и все в порядке.
with open_file('readme.txt') as managed_file:
text = managed_file.read()
print(text)
Как видите, декорированная функция open_file возвращает диспетчер контекста, и мы можем использовать его напрямую. Это позволяет без лишних хлопот достичь того же эффекта, что и при создании класса FileManager.