• 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. Давайте поговорим о том, что происходит под капотом.

  1. Оператор with хранит метод __exit__ класса File.

  2. Он вызывает метод __enter__ класса File.

  3. Метод __enter__ открывает файл и возвращает его.

  4. Ручка открытого файла передается в opened_file.

  5. Мы записываем в файл, используя .write().

  6. Оператор with вызывает сохраненный метод __exit__.

  7. Метод __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 при возникновении ошибки:

  1. Он передает тип, значение и трассировку ошибки в метод __exit__.

  2. Это позволяет методу __exit__ обработать исключение.

  3. Если __exit__ возвращает True, то исключение было изящно обработано.

  4. Если методом 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 и декораторах. В этом примере мы не отлавливали никаких исключений, которые могли бы возникнуть. Он работает в основном так же, как и предыдущий метод.

Давайте немного разберем этот метод.

  1. Python встречает ключевое слово yield. Из-за этого он создает генератор вместо обычной функции.

  2. Благодаря оформлению, contextmanager вызывается с именем функции (open_file) в качестве аргумента.

  3. Декоратор contextmanager возвращает генератор, обернутый объектом GeneratorContextManager.

  4. Объект GeneratorContextManager присваивается функции open_file. Поэтому, когда мы позже вызываем функцию open_file, мы фактически вызываем объект GeneratorContextManager.

Теперь, когда мы все это знаем, мы можем использовать только что созданный Context Manager следующим образом:

with open_file('some_file') as f:
    f.write('hola!')