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

Менеджеры контекста позволяют выделять и освобождать ресурсы строго по необходимости. Самый популярный пример использования менеджера контекста - выражение `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!')
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://pavel-karateev.gitbook.io/intermediate-python/sintaksis/context_managers.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
