← Назад к вопросам

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

2.3 Middle🔥 171 комментариев
#Python Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Контекстные менеджеры без with в Python

Контекстные менеджеры основаны на методах __enter__ и __exit__, которые можно вызывать напрямую без синтаксиса with.

Что такое контекстный менеджер

Это объект, реализующий протокол с двумя методами:

class ContextManager:
    def __enter__(self):
        """Вызывается при входе в блок with."""
        print("Entering context")
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        """Вызывается при выходе из блока with (или исключении)."""
        print("Exiting context")
        if exc_type is not None:
            print(f"Exception: {exc_val}")
        return False  # False = не подавляем исключение

# Обычно:
with ContextManager() as ctx:
    print("Inside context")

# Вывод:
# Entering context
# Inside context
# Exiting context

Способ 1: Прямой вызов методов

Можно вызвать __enter__ и __exit__ явно:

ctx = ContextManager()
try:
    resource = ctx.__enter__()
    print("Using resource")
finally:
    ctx.__exit__(None, None, None)

Пример с файлом:

# С with:
with open("data.txt", "r") as f:
    content = f.read()

# Без with (явный вызов):
f = open("data.txt", "r")
try:
    content = f.read()
finally:
    f.__exit__(None, None, None)  # Закрывает файл

Способ 2: ExitStack для управления несколькими контекстами

Когда нужно управлять несколькими ресурсами без вложенного with:

from contextlib import ExitStack

ctx_manager = ExitStack()
try:
    # Регистрируем ресурсы
    file1 = ctx_manager.enter_context(open("file1.txt", "r"))
    file2 = ctx_manager.enter_context(open("file2.txt", "w"))
    
    # Используем
    content = file1.read()
    file2.write(content.upper())
finally:
    ctx_manager.__exit__(None, None, None)  # Закрывает все ресурсы

Или проще:

from contextlib import ExitStack

ctx = ExitStack()
file1 = ctx.enter_context(open("file1.txt", "r"))
file2 = ctx.enter_context(open("file2.txt", "w"))

content = file1.read()
file2.write(content.upper())

ctx.close()  # Закрывает все

Способ 3: contextmanager декоратор

Для создания контекстного менеджера из генератора:

from contextlib import contextmanager

@contextmanager
def database_connection(db_url):
    # Вызывается при __enter__
    print(f"Connecting to {db_url}")
    conn = Connection(db_url)
    
    try:
        yield conn  # Возвращает ресурс
    finally:
        # Вызывается при __exit__
        print("Closing connection")
        conn.close()

# С with:
with database_connection("postgres://localhost") as conn:
    conn.execute("SELECT * FROM users")

# Без with (явный вызов):
ctx = database_connection("postgres://localhost")
try:
    conn = ctx.__enter__()
    conn.execute("SELECT * FROM users")
finally:
    ctx.__exit__(None, None, None)

Способ 4: asynccontextmanager (для async)

Для асинхронных контекстных менеджеров:

from contextlib import asynccontextmanager

@asynccontextmanager
async def async_database():
    print("Opening async connection")
    conn = await AsyncConnection.create()
    
    try:
        yield conn
    finally:
        print("Closing async connection")
        await conn.close()

# Обычно:
async def main():
    async with async_database() as conn:
        await conn.execute("SELECT * FROM users")

# Без with (явный вызов):
async def main():
    ctx = async_database()
    try:
        conn = await ctx.__aenter__()
        await conn.execute("SELECT * FROM users")
    finally:
        await ctx.__aexit__(None, None, None)

Способ 5: Класс с методами управления ресурсами

Для более сложного случая:

class DatabasePool:
    def __init__(self, url, max_size=5):
        self.url = url
        self.max_size = max_size
        self.pool = []
        self.available = []
    
    def __enter__(self):
        """Подготовка пула."""
        for i in range(self.max_size):
            conn = Connection(self.url)
            self.pool.append(conn)
            self.available.append(conn)
        print(f"Pool initialized with {self.max_size} connections")
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        """Очистка пула."""
        for conn in self.pool:
            conn.close()
        print("Pool cleaned up")
        return False
    
    def get_connection(self):
        """Получить соединение из пула."""
        if not self.available:
            raise RuntimeError("No available connections")
        return self.available.pop()
    
    def return_connection(self, conn):
        """Вернуть соединение в пул."""
        self.available.append(conn)

# Использование без with:
pool = DatabasePool("postgres://localhost", max_size=3)
try:
    pool.__enter__()
    
    conn = pool.get_connection()
    conn.execute("SELECT * FROM users")
    pool.return_connection(conn)
finally:
    pool.__exit__(None, None, None)

Способ 6: suppress для подавления исключений

Встроенный контекстный менеджер для игнорирования исключений:

from contextlib import suppress

# С with:
with suppress(FileNotFoundError):
    open("missing.txt").read()

# Без with (вручную):
ctx = suppress(FileNotFoundError)
try:
    ctx.__enter__()
    open("missing.txt").read()
finally:
    ctx.__exit__(FileNotFoundError, FileNotFoundError("file"), None)

Практический пример: Lock без with

import threading

lock = threading.Lock()

# С with:
with lock:
    print("Critical section")

# Без with:
lock.__enter__()
try:
    print("Critical section")
finally:
    lock.__exit__(None, None, None)

Управление исключениями в exit

Метод __exit__ получает информацию об исключении:

class ErrorHandler:
    def __enter__(self):
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is None:
            # Нет исключения
            print("Success")
        elif exc_type == ValueError:
            # Обработка ValueError
            print(f"ValueError: {exc_val}")
            return True  # Подавляем исключение
        elif exc_type == KeyError:
            # Обработка KeyError
            print(f"KeyError: {exc_val}")
            return False  # Пробросим исключение
        else:
            # Другие исключения
            print(f"Other error: {exc_type}")
            return False
        return False

# Использование:
ctx = ErrorHandler()
try:
    ctx.__enter__()
    # Код, который может выбросить исключение
    x = {}
    value = x["missing"]  # KeyError
except BaseException as e:
    # Передаём информацию об исключении в __exit__
    result = ctx.__exit__(type(e), e, e.__traceback__)
    if not result:
        raise  # Пробросим исключение

Лучшие практики

  • Используйте with когда возможно — это безопаснее и понятнее
  • ExitStack для управления несколькими ресурсами
  • contextmanager для простых случаев
  • enter должен возвращать ресурс, который будет использован
  • exit всегда вызывается, даже при исключении (как finally)
  • Return True в exit подавляет исключение
  • Избегайте сложной логики в exit — это может скрыть реальные ошибки

Контекстные менеджеры — мощный инструмент для управления ресурсами, но без with их вызов становится более подвержен ошибкам.