← Все вопросы

Как написать свой контекстный менеджер с __enter__/__exit__ (а не просто with open)?

Задан 17 месяцев назад1.4к просмотров2 ответа
6

С with open(...) всё ясно, файл сам закрывается. А хочу сделать СВОЙ контекстный менеджер — например, замерять время выполнения блока или временно менять рабочую директорию. Понимаю, что нужны какие-то __enter__ и __exit__, но не соображу, как их правильно написать и что делать с исключениями внутри блока.

2 ответа

11
✓ Принятый ответ — помог автору

Есть два способа. Базовый — класс с методами __enter__ и __exit__. То, что возвращает __enter__, попадает в переменную после as; __exit__ вызывается на выходе из блока ВСЕГДА, даже если внутри вылетело исключение.

import time

class Timer:
    def __enter__(self):
        self.start = time.perf_counter()
        return self                       # это уйдёт в 'as t'

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.elapsed = time.perf_counter() - self.start
        print(f"заняло {self.elapsed:.3f} c")
        return False                      # False -> исключение НЕ глушим, пробросится дальше

with Timer() as t:
    sum(range(10_000_000))

Про __exit__: в него приходят три аргумента про исключение. Если внутри блока всё прошло гладко — они None. Если вылетело исключение — там тип/значение/traceback. Вернёшь из __exit__ True — исключение «проглотится», вернёшь False/None — пробросится наверх (обычно нужно именно это).

Второй способ, короче, — декоратор @contextmanager из contextlib. Пишешь обычную функцию-генератор: всё до yield — это __enter__, всё после — __exit__, а try/finally гарантирует уборку даже при ошибке:

from contextlib import contextmanager
import os

@contextmanager
def cd(path):
    old = os.getcwd()
    os.chdir(path)            # вход
    try:
        yield                 # тело with выполняется здесь
    finally:
        os.chdir(old)         # выход — вернёт директорию даже при исключении

with cd("/tmp"):
    print(os.getcwd())        # /tmp
print(os.getcwd())            # вернулись обратно

Для простых случаев @contextmanager обычно компактнее и читаемее. Класс берут, когда у менеджера есть состояние/несколько методов или его удобнее переиспользовать как объект.

3

Подводный камень с @contextmanager: оборачивай yield в try/finally, иначе при исключении в теле with код «после yield» (твоя уборка) просто не выполнится. Именно finally делает контекстный менеджер надёжным — это его главный смысл, гарантированная очистка ресурса.

И маленькое: один объект-генераторного @contextmanager нельзя использовать в двух with подряд — он одноразовый. Вызывай функцию заново на каждый with.

Ваш ответ

Войдите, чтобы ответить на вопрос.
Поддержать проект