Контекстные менеджеры и оператор with в Python
Для чего используется оператор with? Он помогает упростить некоторые распространенные шаблоны управления ресурсами, абстрагируя их функциональность и позволяя использовать их повторно. В свою очередь, это способствует написанию более информативного кода и помогает избежать утечки ресурсов в ваших программах.
Хороший способ убедиться в эффективности использования этой функции - просмотреть примеры в стандартной библиотеке Python. Хорошо известный пример включает функцию open():
with open('hello.txt', 'w') as f:
f.write('hello, world!')
Обычно рекомендуется открывать файлы с помощью инструкции with, поскольку это гарантирует, что открытые файловые дескрипторы будут автоматически закрыты после того, как выполнение программы выйдет из контекста инструкции with. Внутренне приведенный выше пример кода выглядит примерно так:
f = open('hello.txt', 'w')
try:
f.write('hello, world')
finally:
f.close()
Вы уже можете сказать, что это довольно громозко. Обратите внимание, что оператор try-finally имеет большое значение. Было бы недостаточно просто написать что-то вроде этого:
f = open('hello.txt', 'w')
f.write('hello, world')
f.close()
Эта реализация не гарантирует, что файл будет закрыт, если во время вызова функции f.write() возникнет исключение, и, следовательно, в нашей программе может произойти утечка файлового дескриптора. Вот почему оператор with так полезен. Он упрощает получение и высвобождение ресурсов надлежащим образом.
Другим хорошим примером эффективного использования оператора with в стандартной библиотеке Python является класса threading.Lock:
some_lock = threading.Lock()
# Плохой пример
some_lock.acquire()
try:
# Здесь будет код
finally:
some_lock.release()
# Отличная реализация:
with some_lock:
# Здесь будет код
В обоих случаях использование оператора with позволяет абстрагироваться от большей части логики обработки ресурсов. Вместо того, чтобы каждый раз писать явный оператор try-finally, with позаботится об этом за нас.
Оператор with может сделать код, работающий с системными ресурсами, более читабельным. Он также помогает избежать ошибок или утечек, поскольку практически невозможно забыть об очистке или освобождении ресурса после того, как мы закончим работу с ним.
Поддержка with в своих объектах
Итак, нет ничего сверхестественного в функции open() или классе threading.Lock и в том факте, что их можно использовать с оператором with. Вы можете обеспечить ту же функциональность в своих классах и функциях, реализовав так называемые контекстные менеджеры.
Что такое контекстный менеджер? Это простой протокол (или интерфейс), которому должен следовать ваш объект, чтобы его можно было использовать с инструкцией 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('hello, world!')
... f.write('bye now')
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('hello, world!')
... f.write('bye now')
В этом случае managed_file() - это генератор, который сначала получает ресурс. Затем он временно приостанавливает собственное выполнение и возвращает ресурс, чтобы его мог использовать вызывающий объект. Когда вызывающий объект покидает контекст with, генератор продолжает выполняться, так что могут быть выполнены все оставшиеся шаги по очистке, и ресурс будет возвращен обратно в систему.
Обе реализации, основанные на классах, и реализации, основанные на генераторах, практически эквивалентны. В зависимости от того, какая из них кажется вам более удобочитаемой, вы можете предпочесть одну из них другой.
Недостатком реализации на основе @contextmanager может быть то, что она требует понимания продвинутых концепций Python, таких как декораторы и генераторы.
Еще раз подчеркну, что правильный выбор здесь зависит от того, что вам и вашей команде удобно использовать и что вы считаете наиболее читаемым.
Написание красивых API-интерфейсов с помощью контекстных Менеджеров
Контекстные менеджеры достаточно гибки, и если вы творчески подойдете к использованию инструкции with, то сможете определить удобные API для своих модулей и классов.
Например, что, если бы ресурсом, которым мы хотели управлять, были уровни отступов от текста в какой-нибудь программе-генераторе отчетов? Что, если бы мы могли написать для этого подобный код:
with Indenter() as indent:
indent.print('hi!')
with indent:
indent.print('hello')
with indent:
indent.print('bonjour')
indent.print('hey')
Это почти похоже на использование доменного языка (DSL) для выделения отступов в тексте. Кроме того, обратите внимание, что этот код несколько раз вводит и выводит один и тот же контекстный менеджер, чтобы изменить уровни отступов. Выполнение этого фрагмента кода должно привести к следующему результату и печати аккуратно отформатированного текста:
hi!
hello
bonjour
hey
Как бы вы реализовали контекстный менеджер для поддержки данного функционала?
Кстати, это может стать отличным упражнением, которое поможет вам разобраться в том, как работают контекстные менеджеры. Итак, прежде чем вы ознакомитесь с моей реализацией, приведенной ниже, вы можете потратить некоторое время и попробовать реализовать это самостоятельно в качестве учебного упражнения.
Вот как мы могли бы реализовать эту функциональность, используя контекстный менеджер на основе классов:
class Indenter:
def __init__(self):
self.level = 0
def __enter__(self):
self.level += 1
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.level -= 1
def print(self, text):
print(' ' * self.level + text)
Еще одним хорошим упражнением была бы попытка реорганизовать этот код, чтобы он был основан на генераторе.
Что нужно запомнить
- Оператор with упрощает обработку исключений, инкапсулируя стандартное использование операторов try/finally в так называемых контекстных менеджерах.
- Чаще всего он используется для управления безопасным получением и освобождением системных ресурсов. Ресурсы собираются оператором with и освобождаются автоматически, когда выполнение выходит за рамки контекста with.
- Эффективное использование with может помочь вам избежать утечки ресурсов и упростить чтение вашего кода.
Комментарии
Для того чтобы оставить свое мнение, необходимо зарегистрироваться на сайте