- en
- Language: ru
- Documentation version: latest
27. Контекстные менеджеры¶
Контекстные менеджеры позволяют вам выделять и освобождать ресурсы именно тогда, когда вы этого хотите. Наиболее широко используемым примером контекстных менеджеров является оператор with
. Предположим, у вас есть две связанные операции, которые вы хотели бы выполнить как пару, с блоком кода между ними. Контекстные менеджеры позволяют сделать именно это. Например:
with open('some_file', 'w') as opened_file:
opened_file.write('Hola!')
Приведенный выше код открывает файл, записывает в него некоторые данные, а затем закрывает его. Если во время записи данных в файл произошла ошибка, он пытается закрыть его. Приведенный выше код эквивалентен:
file = open('some_file', 'w')
try:
file.write('Hola!')
finally:
file.close()
Сравнивая его с первым примером, мы видим, что много шаблонного кода устраняется просто за счет использования with
. Основное преимущество использования оператора with
заключается в том, что он обеспечивает закрытие нашего файла, не обращая внимания на то, как завершается вложенный блок.
Частым случаем использования менеджеров контекста является блокировка и разблокировка ресурсов и закрытие открытых файлов (как я уже показывал).
Давайте посмотрим, как мы можем реализовать наш собственный Контекстный менеджер. Это позволит нам понять, что именно происходит за кулисами.
27.1. Реализация контекстного менеджера в виде класса:¶
По крайней мере, у контекстного менеджера определены методы __enter__
и __exit__
. Давайте создадим свой собственный контекстный менеджер для открытия файлов и изучим основы.
class File(object):
def __init__(self, file_name, method):
self.file_obj = open(file_name, method)
def __enter__(self):
return self.file_obj
def __exit__(self, type, value, traceback):
self.file_obj.close()
Просто определив методы __enter__
и __exit__
, мы можем использовать наш новый класс в операторе with
. Давайте попробуем:
with File('demo.txt', 'w') as opened_file:
opened_file.write('Hola!')
Наш метод __exit__
принимает три аргумента. Они требуются каждому методу __exit__
, который является частью класса Context Manager. Давайте поговорим о том, что происходит под капотом.
Оператор
with
хранит метод__exit__
классаFile
.Он вызывает метод
__enter__
классаFile
.Метод
__enter__
открывает файл и возвращает его.Ручка открытого файла передается в
opened_file
.Мы записываем в файл, используя
.write()
.Оператор
with
вызывает сохраненный метод__exit__
.Метод
__exit__
закрывает файл.
27.2. Обработка исключений¶
Мы не говорили об аргументах type
, value
и traceback
метода __exit__
. Между 4-м и 6-м шагом, если возникает исключение, Python передает тип, значение и трассировку исключения методу __exit__
. Это позволяет методу __exit__
решить, как закрыть файл и нужны ли дальнейшие шаги. В нашем случае мы не обращаем на них никакого внимания.
Что делать, если наш файловый объект вызывает исключение? Возможно, мы пытаемся получить доступ к методу объекта файла, который он не поддерживает. Например:
with File('demo.txt', 'w') as opened_file:
opened_file.undefined_function('Hola!')
Перечислим шаги, которые предпринимает оператор with
при возникновении ошибки:
Он передает тип, значение и трассировку ошибки в метод
__exit__
.Это позволяет методу
__exit__
обработать исключение.Если
__exit__
возвращаетTrue
, то исключение было изящно обработано.Если методом
True
возвращается что-либо, отличное от__exit__
, то исключение вызывается операторомwith
.
В нашем случае метод __exit__
возвращает None
(если оператор возврата не встречается, то метод возвращает None
). Поэтому оператор with
вызывает исключение:
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
AttributeError: 'file' object has no attribute 'undefined_function'
Попробуем обработать исключение в методе __exit__
:
class File(object):
def __init__(self, file_name, method):
self.file_obj = open(file_name, method)
def __enter__(self):
return self.file_obj
def __exit__(self, type, value, traceback):
print("Exception has been handled")
self.file_obj.close()
return True
with File('demo.txt', 'w') as opened_file:
opened_file.undefined_function()
# Output: Exception has been handled
Наш метод __exit__
возвращал True
, поэтому оператор with
не вызвал исключения.
Это не единственный способ реализации менеджеров контекста. Есть и другой способ, и мы рассмотрим его в следующем разделе.
27.3. Реализация контекстного менеджера в качестве генератора¶
Мы также можем реализовать менеджеры контекста с помощью декораторов и генераторов. В Python есть модуль contextlib, предназначенный именно для этой цели. Вместо класса мы можем реализовать контекстный менеджер с помощью функции-генератора. Рассмотрим базовый, бесполезный пример:
from contextlib import contextmanager
@contextmanager
def open_file(name):
f = open(name, 'w')
try:
yield f
finally:
f.close()
Хорошо! Этот способ реализации контекстных менеджеров кажется более интуитивным и простым. Однако этот метод требует некоторых знаний о генераторах, yield и декораторах. В этом примере мы не отлавливали никаких исключений, которые могли бы возникнуть. Он работает в основном так же, как и предыдущий метод.
Давайте немного разберем этот метод.
Python встречает ключевое слово
yield
. Из-за этого он создает генератор вместо обычной функции.Благодаря оформлению, contextmanager вызывается с именем функции (
open_file
) в качестве аргумента.Декоратор
contextmanager
возвращает генератор, обернутый объектомGeneratorContextManager
.Объект
GeneratorContextManager
присваивается функцииopen_file
. Поэтому, когда мы позже вызываем функциюopen_file
, мы фактически вызываем объектGeneratorContextManager
.
Теперь, когда мы все это знаем, мы можем использовать только что созданный Context Manager следующим образом:
with open_file('some_file') as f:
f.write('hola!')