> For the complete documentation index, see [llms.txt](https://pavel-karateev.gitbook.io/intermediate-python/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://pavel-karateev.gitbook.io/intermediate-python/sintaksis/context_managers.md).

# Менеджеры контекста

Менеджеры контекста позволяют выделять и освобождать ресурсы строго по необходимости. Самый популярный пример использования менеджера контекста - выражение `with`. Предположим, у вас есть две связанные операции, которые вы хотите исполнить в паре, поместив между ними блок кода. Менеджеры контекста позволяют сделать именно это. Например:

```python
with open('some_file', 'w') as opened_file:
    opened_file.write('Hola!')
```

Код выше открывает файл, записывает в него данные и закрывает файл после этого. При возникновении ошибки при записи данных в файл менеджер контекста попробует его закрыть. Этот код эквивалентен следующему:

```python
file = open('some_file', 'w')
try:
    file.write('Hola!')
finally:
    file.close()
```

Сравнив с первым блоком кода, мы можем заметить замену шаблонного кода на `with`. Основное преимущество использования `with` - это гарантия закрытия файла вне зависимости от того, как будет завершён вложенный код.

Распространенный паттерн использования контекстных менеджеров - блокирование и разблокирование ресурсов, а также закрытие открытых файлов (как я уже показал выше).

Давайте посмотрим, как мы можем написать свой собственный менеджер контекста. Это позволит нам лучше понять логику его работы.

## Контекст-менеджер как класс

Необходимый минимум функциональности контекстного менеджера требует методов `__enter__` и `__exit__`. Давайте напишем свой контекстный менеджер для работы с файлами и изучим основы.

```python
    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`. Давайте попробуем:

```python
with File('demo.txt', 'w') as opened_file:
    opened_file.write('Hola!')
```

Метод `__exit__` принимает три аргумента. Они обязательны для любого метода `__exit__` класса контекстного менеджера. Давайте обсудим логику работы:

1. `with` сохраняет метод `__exit__` класса `File`.
2. Следует вызов метода `__enter__` класса `File`.
3. Метод `__enter__` открывает файл и возвращает его.
4. Дескриптор файла передается в `opened_file`.
5. Мы записываем информацию в файл при помощи `.write()`.
6. `with` вызывает сохраненный `__exit__` метод.
7. Метод `__exit__` закрывает файл.

## Обработка исключений

Мы ещё не успели поговорить об аргументах `type`, `value` и `traceback` метода `__exit__`. Между четвертым и шестым шагом при возникновении исключения, Python передает тип, значение и обратную трассировку исключения методу `__exit__`. Это позволяет методу `__exit__` выбирать способ закрытия файла и выполнять дополнительные действия при необходимости. В нашем случае, мы не уделяем им особого внимания.

Что если объект файла вызвал исключение? Возможно, мы пытаемся вызывать метод на объекте, который его не поддерживает. Например:

```python
with File('demo.txt', 'w') as opened_file:
    opened_file.undefined_function('Hola!')
```

Давайте разберём шаги, которые выполняет `with` при возникновении исключения:

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

   `__exit__`.
2. Обработка исключения передается методу `__exit__`
3. Если `__exit__` возвращает `True`, то исключение было корректно обработано.
4. При возврате любого другого значения `with` вызывает исключение.

В нашем случае метод `__exit__` возвращает `None` (при отсутствии выражения `return` метод в Python возвращает `None`). Таким образом, `with` вызывает исключение:

```python
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
AttributeError: 'file' object has no attribute 'undefined_function'
```

Давайте попробуем обработать исключение в методе `__exit__`:

```python
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("Исключение было обработано")
        self.file_obj.close()
        return True

with File('demo.txt', 'w') as opened_file:
    opened_file.undefined_function()

# Вывод: Исключение было обработано
```

Наш метод `__exit__` возвращает `True`, таким образом `with` не вызывает исключение.

Это не единственный способ реализации контекстных менеджеров - есть и другой и мы посмотрим на него в следующем параграфе.

## Контекст-менеджер из генератора

Мы также можем реализовать менеджер контекста через декораторы и генераторы. В Python присутствует модуль `contextlib` специально для этой цели. Вместо написания класса, мы можем реализовать менеджер контекста из функции-генератора. Посмотрим на простой пример:

```python
from contextlib import contextmanager

@contextmanager
def open_file(name):
    f = open(name, 'w')
    yield f
    f.close()
```

Отлично! Реализация менеджера контекста таким способом смотрится более интуитивной и простой. Тем не менее, этот метод требует определённых знаний о генераторах, `yield` и декораторах. В примере выше мы не обрабатываем возможные исключения. В целом, он почти такой же, что и предыдущий.

Давайте чуть подробнее разберем этот подход:

1. Python встречает ключевое слово `yield`. Благодаря этому он создает

   генератор, а не простую функцию.
2. Благодаря декоратору, `contextmanager` вызывается с функцией

   `open_file` в качестве аргумента.
3. Функция `contextmanager` возвращает генератор, обёрнутый в объект

   `GeneratorContextManager`.
4. `GeneratorContextManager` присваивается функции `open_file`. Таким

   образом, когда мы вызовем функцию `open_file` в следующий раз, то

   фактически обратимся к объекту `GeneratorContextManager`.

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

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