Как реализовать собственный контекстный менеджер двумя способами?

Контекстный менеджер можно реализовать как класс с методами __enter__/__exit__, а также как функцию‑генератор с декоратором contextlib.contextmanager, где yield разделяет код инициализации и финализации.
Подробный ответ

Классический способ: класс с __enter__ и __exit__

Контекстный менеджер — это объект, реализующий методы __enter__(self) и __exit__(self, exc_type, exc, tb). Конструкция with вызывает эти методы по протоколу:

  • перед входом в блок with вызывается __enter__; его результат присваивается переменной после as;

  • при выходе из блока (нормальном или по исключению) вызывается __exit__;

  • если __exit__ возвращает True, исключение подавляется.

class managed_resource:
   def __init__(self, name):
       self.name = name
       self.handle = None
       
   def __enter__(self):
       print(f"acquire {self.name}")
       self.handle = open(self.name, "w")
       return self.handle
       
   def __exit__(self, exc_type, exc, tb):
       print(f"release {self.name}")
       if self.handle:
           self.handle.close()
       # вернём False, чтобы не подавлять исключения
       return False

with managed_resource("log.txt") as f:
   f.write("hello")

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

Функциональный способ: contextlib.contextmanager

Второй способ — описать контекстный менеджер как генератор и обернуть его @contextlib.contextmanager. В этом случае:

  • код до yield соответствует __enter__;

  • значение, переданное через yield, — это то, что попадёт в as;

  • код после yield — это __exit__; он выполнится всегда, даже при исключении.

from contextlib import contextmanager

@contextmanager
def managed_resource(name):
   print(f"acquire {name}")
   f = open(name, "w")
   try:
       yield f  # сюда попадает переменная после 'as'
   finally:
       print(f"release {name}")
       f.close()

with managed_resource("log.txt") as f:
   f.write("hello")

Такой способ чаще используют, когда нужен лёгкий одноразовый менеджер без отдельного класса, особенно когда логика инициализации/финализации компактна.

Отличия и выбор

  • класс даёт больше контроля, возможности наследования, хранения состояния и тестирования по отдельности;

  • генераторный подход с @contextmanager — более лаконичен для простых кейсов, но скрывает детали протокола и чуть сложнее отлаживается при большой логике.

Формулировка для собеседования

Свой контекстный менеджер можно реализовать классом с методами __enter__/__exit__ или функцией‑генератором под @contextmanager. В первом случае мы явно работаем с протоколом, во втором — используем более декларативный синтаксис: всё, что до yield, — настройка, всё, что после, — гарантированная финализация ресурса в блоке finally.

Оцени свой прогресс

Честно оцени своё понимание этого вопроса, чтобы мы могли построить твой учебный трек максимально эффективно.
Читать в блоге