Вступление
Одна из самых «непонятных» функций Python, которую используют почти все
программисты Python, даже начинающие, но не очень понимающие, - это
менеджеры контекста . Вы, наверное, видели их в форме with
, которые
обычно впервые встречаются, когда вы изучаете открытие файлов в Python.
Хотя менеджеры контекста сначала кажутся немного странными, когда мы
действительно погружаемся в них, понимаем мотивацию и методы, стоящие за
ними, мы получаем доступ к новому оружию в нашем арсенале
программирования. Итак, без лишних слов, давайте погрузимся в это!
Мотивация: Управление ресурсами
Как сказал кто-то гораздо более мудрый, чем я: «Необходимость - мать изобретений». Чтобы действительно понять, что такое диспетчер контекста и как мы можем его использовать, мы должны сначала исследовать мотивы, стоящие за ним - потребности, которые привели к этому «изобретению».
Основная мотивация менеджеров контекста - это управление ресурсами. Когда программа хочет получить доступ к ресурсу на компьютере, она запрашивает об этом у ОС, а ОС, в свою очередь, предоставляет ей дескриптор этого ресурса. Некоторыми распространенными примерами таких ресурсов являются файлы и сетевые порты. Важно понимать, что эти ресурсы имеют ограниченную доступность, например, сетевой порт может использоваться одним процессом за раз, а количество доступных портов ограничено. Поэтому всякий раз, когда мы открываем ресурс, мы должны помнить о его закрытии , чтобы ресурс был освобожден. Но, к сожалению, легче сказать, чем сделать.
Самый простой способ добиться правильного управления ресурсами - это
вызвать close
после того, как мы закончим с ресурсом. Например:
opened_file = open('readme.txt')
text = opened_file.read()
...
opened_file.close()
Здесь мы открываем файл с именем readme.txt
, читаем файл и сохраняем
его содержимое в виде строки text
, а затем, когда мы закончили с ним,
закрываем файл, вызывая метод close()
объекта opened_file
На первый
взгляд это может показаться нормальным, но на самом деле это совсем не
надежно. Если между открытием файла и закрытием файла произойдет
что-то неожиданное, в результате чего программа не сможет выполнить
строку, содержащую close
, произойдет утечка ресурсов. Эти
неожиданные события - это то, что мы называем exceptions
, обычно
случаются, когда кто-то принудительно закрывает программу во время ее
выполнения.
Теперь правильный способ справиться с этим - использовать обработку
исключений с использованием блоков 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__
-
это метод, который вызывается, когда мы открываем ресурс, или, говоря
несколько более технически, - когда мы «входим» в контекст выполнения
. Оператор 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
объекта и присваивает
возвращенное значение в 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 позаботится об
управлении ресурсами, где бы этот класс ни использовался. Это подводит
меня к следующей теме, различным способам использования классов
диспетчера контекста, таких как этот класс 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
Важно : есть некоторые тонкости, которые я упустил, чтобы обсуждение было предметным. Точные спецификации этих волшебных методов см. В документации Python здесь .
Использование contextlib
Дзен Python - руководящий принцип Python в виде списка афоризмов - гласит, что:
Лучше простое, чем сложное.
Чтобы действительно понять этот момент, разработчики Python создали библиотеку с именем contextlib, содержащую утилиты для менеджеров контекста, как будто они недостаточно упростили проблему управления ресурсами. Я собираюсь кратко продемонстрировать здесь только один из них, я рекомендую вам ознакомиться с официальной документацией Python, чтобы узнать больше.
from contextlib import contextmanager
@contextmanager
def open_file(filename):
opened_file = open(filename)
try:
yield opened_file
finally:
opened_file.close()
Как и в приведенном выше коде, мы можем просто определить функцию,
которая yield
защищенный ресурс в операторе try
, закрывая его в
последующем операторе finally
Другой способ понять это:
- Все содержимое, которое вы в противном случае поместили бы в
__enter__
, за исключениемreturn
, находитсяtry
- в основном это инструкции по открытию ресурса. - Вместо того, чтобы возвращать ресурс, вы
yield
его внутри блокаtry
- Содержимое
__exit__
внутри соответствующего блокаfinally
Когда у нас есть такая функция, мы можем украсить ее с
contextlib.contextmanager
и все в порядке.
with open_file('readme.txt') as managed_file:
text = managed_file.read()
print(text)
Как видите, декорированная open_file
возвращает диспетчер контекста, и
мы можем использовать его напрямую. Это позволяет без лишних хлопот
достичь того же эффекта, что и при создании FileManager
дальнейшее чтение
Если вы чувствуете энтузиазм и хотите узнать больше о менеджерах контекста, я рекомендую вам проверить следующие ссылки: