Рубрики
Без рубрики

Контекстные менеджеры Python

Автор оригинала: Guest Contributor.

Контекстные менеджеры Python

Вступление

Одна из самых “неясных” функций Python, которую используют почти все программисты Python, даже начинающие, но на самом деле не понимают, – это контекстные менеджеры . Вы, вероятно, видели их в виде операторов with , обычно впервые встречающихся при изучении открытия файлов в Python. Хотя поначалу контекстные менеджеры кажутся немного странными, когда мы действительно погружаемся в них, понимаем мотивацию и методы, стоящие за ними, мы получаем доступ к новому оружию в нашем арсенале программирования. Так что без лишних слов давайте нырнем в него!

Мотивация: Управление ресурсами

Как сказал кто-то гораздо более мудрый, чем я, “Необходимость-мать изобретения”. Чтобы действительно понять, что такое контекстный менеджер и как его можно использовать, мы должны сначала исследовать мотивы, стоящие за ним, — потребности, которые породили это “изобретение”.

Основной мотивацией контекстных менеджеров является управление ресурсами. Когда программа хочет получить доступ к ресурсу на компьютере, она запрашивает его у ОС, а ТА, в свою очередь, предоставляет ей дескриптор этого ресурса. Наиболее распространенными примерами таких ресурсов являются файлы и сетевые порты. Важно понимать, что эти ресурсы имеют ограниченную доступность, например, сетевой порт может использоваться одним процессом одновременно, а количество доступных портов ограничено. Поэтому всякий раз, когда мы открываем ресурс, мы должны помнить закрыть его, чтобы ресурс был освобожден. Но, к сожалению, это легче сказать, чем сделать.

Самый простой способ добиться правильного управления ресурсами-это вызвать функцию close после того, как мы закончим с ресурсом. Например:

opened_file = open('readme.txt')
text = opened_file.read()
...
opened_file.close()

Здесь мы открываем файл с именем readme.txt , читая файл и сохраняя его содержимое в строке text , а затем, когда мы закончим с ним, закрываем файл, вызывая метод close() объекта opened_file . На первый взгляд это может показаться нормальным, но на самом деле это совсем не надежно. Если между открытием и закрытием файла произойдет что-либо непредвиденное, что приведет к тому, что программа не сможет выполнить строку, содержащую оператор close , произойдет утечка ресурсов. Эти неожиданные события являются тем , что мы называем исключениями , распространенным является случай, когда кто-то принудительно закрывает программу во время ее выполнения.

Теперь правильным способом справиться с этим было бы использовать Обработку исключений , используя try...else блоки. Посмотрите на следующий пример:

try:
    opened_file = open('readme.txt')
    text = opened_file.read()
    ...
else:
    opened_file.close()

Python всегда следит за тем, чтобы код в блоке else был выполнен, независимо от того, что может произойти. Именно так программисты на других языках справлялись бы с управлением ресурсами, но программисты на Python получают специальный механизм, который позволяет им реализовать ту же функциональность без всяких шаблонных шаблонов. Именно здесь в игру вступают контекстные менеджеры.

Внедрение Контекстных менеджеров

Теперь, когда мы закончили с самой важной частью понимания контекстных менеджеров, мы можем перейти к их реализации. В этом уроке мы реализуем пользовательский класс File . Это совершенно излишне, поскольку Python уже предоставляет это, но, тем не менее, это будет хорошее учебное упражнение, поскольку мы всегда сможем вернуться к классу File , который уже есть в стандартной библиотеке.

Стандартный и “низкоуровневый” способ реализации контекстного менеджера-это определение двух “волшебных” методов в классе, для которого вы хотите реализовать управление ресурсами, __enter__ | и _ _ exit__|/. Если вы теряетесь, думая: “Что это за волшебный метод? Я никогда не слышал об этом раньше”—ну, если вы начали заниматься объектно-ориентированным программированием на Python, вы наверняка уже сталкивались с волшебным методом, методом __init__ .

За неимением лучших слов, это специальные методы, которые вы можете определить, чтобы сделать ваши классы умнее или добавить к ним “волшебство”. Вы можете найти хороший справочный список всех магических методов, доступных в Python здесь .

В любом случае, возвращаясь к теме, прежде чем мы начнем применять эти два магических метода, мы должны понять их назначение. __enter__ — это метод, который вызывается, когда мы открываем ресурс, или, выражаясь несколько более техническим способом-когда мы “входим” в контекст runtime . Оператор 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__ открывает ресурс—файл—и возвращает его. Когда мы используем этот файловый менеджер в операторе with , этот метод будет вызван, и его возвращаемое значение будет привязано к целевой переменной, упомянутой вами в предложении as . Я продемонстрировал это в следующем фрагменте кода:

with FileManager('readme.txt') as file:
    text = file.read()

Давайте разберем его по частям. Во – первых, экземпляр класса FileManager создается, когда мы создаем его экземпляр, передавая имя файла “readme.txt-к конструктору. Затем оператор with начинает работать над ним — он вызывает метод __enter__ | этого объекта FileManager и присваивает возвращаемое значение переменной file , упомянутой в предложении as . Затем внутри блока with мы можем делать с открытым ресурсом все, что захотим.

Другой важной частью головоломки является метод __exit__ . Метод __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 позаботится об управлении ресурсами везде, где этот класс может быть использован. Это подводит меня к следующей теме-различным способам использования классов context manager, таким как этот File Manager class.

Использование Контекстных менеджеров

Здесь не так много нужно объяснять, поэтому вместо того, чтобы писать длинные абзацы, я приведу несколько фрагментов кода в этом разделе:

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 .

Важно : Есть некоторые тонкости, которые я опустил, чтобы сделать дискуссию по существу. Для получения точных спецификаций этих магических методов обратитесь к документации Python здесь .

Использование contextlib

Дзен Питона —руководящий принцип Питона как список афоризмов—гласит, что,

Простое лучше, чем сложное.

Чтобы действительно довести эту точку зрения до конца, разработчики Python создали библиотеку с именем contextlib , содержащую утилиты, касающиеся контекстных менеджеров, как будто они недостаточно упростили проблему управления ресурсами. Я собираюсь кратко продемонстрировать здесь только один из них, я рекомендую вам ознакомиться с официальными документами Python для получения дополнительной информации.

from contextlib import contextmanager

@contextmanager
def open_file(filename):
    opened_file = open(filename)
    try:
        yield opened_file
    finally:
        opened_file.close()

Как и в приведенном выше коде, мы можем просто определить функцию, которая дает s защищенный ресурс в операторе try , закрывая его в последующем операторе finally . Другой способ понять это:

  • Все содержимое, которое вы иначе поместили бы в метод __enter__ , за исключением оператора return , идет перед блоком try здесь — в основном инструкции по открытию ресурса.
  • Вместо того чтобы возвращать ресурс, вы отдаете его внутри блока try .
  • Содержимое метода __exit__ входит в соответствующий блок finally .

Как только у нас есть такая функция, мы можем украсить ее с помощью contextlib.contextmanager decorator, и у нас все хорошо.

with open_file('readme.txt') as managed_file:
    text = managed_file.read()
    print(text)

Как вы можете видеть, функция decorated open_file возвращает контекстный менеджер, и мы можем использовать его напрямую. Это позволяет нам достичь того же эффекта, что и при создании класса FileManager , без всяких хлопот.

Дальнейшее чтение

Если вы чувствуете энтузиазм и хотите узнать больше о контекстных менеджерах, я рекомендую вам ознакомиться со следующими ссылками: