Менеджер контекста в Python

Одна из самых «непонятных» функций 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)

Как видите, главное помнить:

  1. Объект, переданный в оператор with, должен иметь методы __enter__ и __exit__.
  2. Метод __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.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *