Создание собственных контекстных менеджеров в Python

19 июня 2026
Автор

Антон Ларичев

Введение

В предыдущей статье мы разобрали, как работает оператор with и зачем он нужен. Но Python позволяет не только пользоваться встроенными контекстными менеджерами — ты можешь создавать свои собственные. Это полезно, когда нужно автоматизировать управление ресурсами, замерять время выполнения, управлять транзакциями или временно менять состояние программы.

В этой статье разберём, как создать контекстный менеджер через класс с методами __enter__ и __exit__, и рассмотрим практические примеры.

Протокол контекстного менеджера

Чтобы объект можно было использовать в операторе with, у него должны быть два метода:

class MyManager:
    def __enter__(self):
        # Вызывается при входе в блок with
        # Возвращаемое значение присваивается переменной после as
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        # Вызывается при выходе из блока with
        # Параметры содержат информацию об исключении (если оно было)
        # Возвращает True для подавления исключения, False — для его проброса
        return False

Параметры метода __exit__:

  • exc_type — тип исключения (например, ValueError) или None, если исключения не было
  • exc_val — само исключение (экземпляр) или None
  • exc_tb — traceback (объект отслеживания стека) или None

Простой пример: замер времени выполнения

Создадим контекстный менеджер, который измеряет время выполнения кода:

import time

class Timer:
    def __enter__(self):
        self.start = time.perf_counter()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.elapsed = time.perf_counter() - self.start
        print(f"Время выполнения: {self.elapsed:.4f} сек")
        return False

# Использование
with Timer() as t:
    # Какая-то тяжёлая операция
    total = sum(range(10_000_000))

print(f"Результат: {total}")
print(f"Замеренное время: {t.elapsed:.4f} сек")

Обрати внимание: __enter__ возвращает self, поэтому после блока with можно обратиться к t.elapsed.

Управление файловым логом

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

from datetime import datetime

class FileLogger:
    def __init__(self, filename):
        self.filename = filename
        self.file = None

    def __enter__(self):
        self.file = open(self.filename, "a", encoding="utf-8")
        self.log(f"--- Сессия начата ---")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is not None:
            self.log(f"ОШИБКА: {exc_type.__name__}: {exc_val}")
        self.log(f"--- Сессия завершена ---\n")
        self.file.close()
        return False  # Не подавляем исключения

    def log(self, message):
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self.file.write(f"[{timestamp}] {message}\n")

# Использование
with FileLogger("app.log") as logger:
    logger.log("Приложение запущено")
    logger.log("Обработка данных...")
    # Если здесь произойдёт ошибка — она запишется в лог
    logger.log("Данные обработаны успешно")

Если вы хотите детальнее изучить ООП и продвинутые возможности Python — приходите на наш большой курс Основы Python. На курсе 209 уроков и 34 упражнения, AI-тренажёры для практики 24/7, решение задач с живым ревью наставника, еженедельные встречи с менторами.

Подавление исключений

Метод __exit__ может подавить исключение, вернув True:

class SuppressErrors:
    def __init__(self, *exceptions):
        self.exceptions = exceptions

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is not None and issubclass(exc_type, self.exceptions):
            print(f"Подавлено исключение: {exc_type.__name__}: {exc_val}")
            return True  # Исключение подавлено
        return False  # Остальные исключения пробрасываем

# Использование
with SuppressErrors(ValueError, ZeroDivisionError):
    result = int("не число")  # ValueError подавлена
    print("Эта строка не выполнится")

print("Программа продолжает работу")

Временное изменение состояния

Контекстный менеджер для временной смены рабочей директории:

import os

class ChangeDir:
    def __init__(self, new_path):
        self.new_path = new_path
        self.old_path = None

    def __enter__(self):
        self.old_path = os.getcwd()
        os.chdir(self.new_path)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        os.chdir(self.old_path)
        return False

# Использование
print(f"До: {os.getcwd()}")

with ChangeDir("/tmp"):
    print(f"Внутри: {os.getcwd()}")

print(f"После: {os.getcwd()}")  # Вернулись в исходную директорию

Контекстный менеджер для транзакций

Пример паттерна «всё или ничего»:

class Transaction:
    def __init__(self, data):
        self.data = data
        self.backup = None

    def __enter__(self):
        # Сохраняем копию данных
        self.backup = self.data.copy()
        return self.data

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is not None:
            # Откатываем изменения при ошибке
            self.data.clear()
            self.data.update(self.backup)
            print("Транзакция откачена — данные восстановлены")
        else:
            print("Транзакция зафиксирована")
        return False

# Использование
user = {"name": "Алиса", "balance": 1000}

# Успешная транзакция
with Transaction(user) as data:
    data["balance"] -= 200
    data["last_purchase"] = "Курс Python"
print(user)  # {'name': 'Алиса', 'balance': 800, 'last_purchase': 'Курс Python'}

# Неуспешная транзакция
try:
    with Transaction(user) as data:
        data["balance"] -= 5000
        if data["balance"] < 0:
            raise ValueError("Недостаточно средств")
except ValueError:
    pass
print(user)  # {'name': 'Алиса', 'balance': 800, 'last_purchase': 'Курс Python'}

Реентерабельные контекстные менеджеры

Некоторые менеджеры можно использовать повторно, другие — нет. Вот пример реентерабельного:

class Indenter:
    def __init__(self):
        self.level = 0

    def __enter__(self):
        self.level += 1
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.level -= 1
        return False

    def print(self, text):
        print("  " * self.level + text)

# Использование — вложенные блоки with
indent = Indenter()

with indent:
    indent.print("Уровень 1")
    with indent:
        indent.print("Уровень 2")
        with indent:
            indent.print("Уровень 3")
        indent.print("Снова уровень 2")
    indent.print("Снова уровень 1")

Вывод:

  Уровень 1
    Уровень 2
      Уровень 3
    Снова уровень 2
  Снова уровень 1

Частые ошибки

  • Забыть вернуть значение из __enter__. Если __enter__ ничего не возвращает, переменная после as будет None:
class Bad:
    def __enter__(self):
        pass  # Забыли return self
    def __exit__(self, *args):
        return False

with Bad() as b:
    print(b)  # None — скорее всего, это не то, что ожидалось
  • Не обрабатывать исключения в __exit__. Если __exit__ сам бросает исключение, оригинальное исключение из блока with будет потеряно.

  • Подавлять все исключения без разбора. Возвращать True из __exit__ нужно осознанно и только для конкретных типов исключений.

Частозадаваемые вопросы

Чем отличается контекстный менеджер через класс от декоратора @contextmanager?

Класс с __enter__/__exit__ — это «полноценный» способ. Декоратор @contextmanager из модуля contextlib — сокращённый вариант через генератор. Класс даёт больше контроля, особенно при сложной логике обработки исключений.

Можно ли наследовать контекстные менеджеры?

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

Когда стоит создавать свой контекстный менеджер?

Когда у тебя есть парная операция «настроить — убрать за собой»: открыть — закрыть, заблокировать — разблокировать, изменить — вернуть обратно. Если ресурс требует обязательной финализации — контекстный менеджер поможет не забыть об этом.

Заключение

Создание собственных контекстных менеджеров — мощный инструмент для написания надёжного и чистого кода на Python. Протокол __enter__/__exit__ прост, но открывает широкие возможности: от замера производительности до управления транзакциями и временного изменения конфигурации.

Для закрепления навыков работы с ООП и продвинутыми возможностями Python рекомендуем курс Основы Python. В первых 3 модулях курса доступно бесплатное содержание, что позволяет попробовать практические задания и понять структуру курса до покупки полного доступа.

Стрелочка влевоФункция super() в Python — как вызвать метод родителяРабота с символами программирования PythonСтрелочка вправо

Постройте личный план изучения Python до уровня Middle — бесплатно!

Python — часть карты развития Backend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Все гайды по Python

Как отправлять запросы с помощью requests в PythonПочему Python выводит значение без команды printКак работает команда print в PythonВозможности Python для автоматизации задачРабота с JSON в Python на примерахPython get — методы получения данныхКак находить и исправлять ошибки в PythonРабота с данными через API и внешние сервисыСтруктура и оформление кода PythonОсновы Django с PythonПолезные приёмы в Python для повседневной работыИспользование locals в Python для отладкиИнтеграция PHP и PythonКак выполнять HTTPS-запросы в PythonFastAPI Python — быстрый старт: создание REST API с нуляКак работать с API в Python
Ввод целого числа в PythonВедение логов в PythonУдаление данных в Python с помощью removeОбработка исключений с помощью try/except в PythonФункция super() в Python — как вызвать метод родителяСоздание собственных контекстных менеджеров в PythonРабота с символами программирования PythonРабота с переменной X в PythonРабота с классами в PythonКак скачать Python на компьютерПростая программа на Python для начинающихОсновы Python для тех, кто начинаетЧто нового в Python 3Поддерживается ли Python 2 и стоит ли его использоватьPython 1 — с чего начиналась история языкаКоманда python print - полное руководство по выводу данныхПравила именования переменных в PythonПользовательские исключения в PythonОсновы Python coreОписание объектов PythonНаследование классов в Python — основы и примерыМножественное наследование в Python — примеры и MROКонтекстный менеджер with в Python — как работает и зачем нуженКомментарии в Python — однострочные, многострочные и docstringКакой Python выбрать для установкиКак вывести целое число с помощью print в PythonКак установить Python на Windows macOS и LinuxКак пользоваться консолью PythonКак получить последний элемент в PythonКак найти значение в PythonКак настроить PythonКак использовать print для строк в PythonКак работает интерпретатор PythonИнструкция по работе с PythonБлок finally в обработке исключений PythonЦелые числа в PythonАбстрактные классы в Python — ABC и abstractmethod
Загрузка данных PythonУправление проектами на GitHub с PythonСоздание веб-приложений на Flask PythonСоздание бота на PythonСоздание интерфейсов Python QTСоздание игр с PygameСоздание GUI в PythonКак работать со словарями в PythonРабота с библиотеками через Python PackagingРабота со временем в Python при помощи модуля timePython name — особенности переменнойМатематические операции в Python с модулем mathPython listing — что это и как использоватьPytest — тестирование на Python: полное руководствоОбработка изображений с OpenCV PythonNumPy в Python — основы и применение в задачахМашинное обучение с PythonИспользование Anaconda с PythonМодуль contextlib в Python — утилиты для контекстных менеджеровБиблиотеки Python и их применение в проектах
Возврат значений из функции в PythonВложенные функции в PythonСоздание собственных декораторов в PythonРабота с функцией map в PythonЦикл while в Python и примеры использованияОбработка чисел, введённых через input в PythonОсновные операторы в Python с примерамиУсловные выражения if else в Python для начинающихКак выполняется вызов функций call в PythonПродвинутые генераторы в Python — send, throw, close и корутиныПозиционные и именованные аргументы в PythonОбъявление переменных и управление областью видимости в PythonПередача аргументов по ссылке и по значению в PythonПередача аргументов через args и kwargs в PythonОсновные методы Python и примеры их использованияОператор match/case в Python 3.10+ — основы структурного сопоставленияПаттерны match/case в Python — деструктуризация, guard и вложенные шаблоныПрактические примеры match/case в Python — реальные сценарии примененияЛокальные и глобальные переменные в PythonЧасто используемые команды PythonКлючевые слова global и nonlocal в PythonКак создавать функции в PythonКак работает сборщик мусора в PythonКак работает область видимости переменных в PythonКак работает функция callable в PythonКак работает функция any и all в PythonКак проверить тип переменной в PythonКак передать функцию как аргумент в PythonКак использовать функцию isinstance в PythonКак использовать функцию filter в PythonКак использовать функцию filter в PythonКак использовать функцию eval безопасно в PythonКак использовать декораторы в PythonИзменяемые и неизменяемые типы данных в PythonГенераторы и yield в Python — как создавать и использоватьГенераторные выражения в Python — синтаксис и примерыФункции в Python и способы их вызоваФункции как объекты в PythonЧто такое замыкания в PythonЧто делает функция reduce в PythonЧто делает функция id в PythonАргументы по умолчанию в PythonАнонимные функции и lambda в PythonАлгоритмы на Python — примеры и объяснение
Запись данных в PythonУстановка pip в PythonУправление зависимостями requirement в PythonУправление библиотеками с помощью Python PackagingУдаление пробелов с помощью strip в PythonСтруктурирование кода в PythonСоздание исполняемого файла Python в exeРазбор traceback в модуле PythonРазбор site-packages в PythonРазбор Program Files в PythonРабота с Unicode кодировками в PythonРабота с системными функциями Python sysРабота с папкой AppData в PythonРабота с модулем logging в PythonРабота с каталогами в PythonРабота с CSV в PythonВиртуальная среда venv в Python — создание и настройкаКак создать простое приложение на PythonИспользование pip в Python для установки пакетовМодули в Python и организация кода в проектеИмпорт модулей в Python и правила подключенияРабота с файлами в Python пошаговоЧто делает компилятор Python и как он работаетПолучение строки из модуля PythonПодключение файлов в Python с includeПеременные среды в PythonСборка проекта с помощью packaging в PythonНастройка Python сервераИспользование Python на UbuntuИспользование консоли PythonИспользование кодировок в PythonИнициализация пакетов PythonИмпорт модулей PythonИмпорт имен в PythonСреда IDLE Python и базовые возможностиЧтение и запись TXT в PythonЧтение файлов в Python с помощью open file
Удаление элементов из списка PythonТипы данных в Python — обзор и рекомендацииОсновные операции со строками в PythonМетоды str в Python и обработка текстаСписки в Python и их ключевые методыСоздание списков данных в PythonРабота со строками и символами в PythonРабота со столбцами в PythonРабота со списком значений в PythonРабота с таблицами в Python с помощью DataFrameРабота с RFR в PythonРабота с пробелами в PythonРабота с массивами в PythonРабота с кортежами tuple PythonРабота с координатами X и Y в PythonРабота с ключами в PythonРабота с элементами данных PythonРабота с двоичными числами PythonРабота с данными в PythonРабота с данными NumPy PythonРабота с большими числами в PythonРабота с битами в PythonРабота с байтами в PythonЧто такое значение в Python и как его определитьМножества в Python и операции с нимиИспользование range в Python для цикловПроверка на четность в PythonПроверка числа в PythonПреобразование типов в PythonПреобразование списка в строку PythonПреобразование числа в строку в PythonПостроение графиков в PythonОпределение индекса элемента в PythonОкругление чисел в PythonОбъединение списков в Python с помощью zipМножества в PythonМассивы в Python и отличие от списковМассив чисел в PythonКортежи данных в PythonКак вычислить сумму чисел в PythonКак получить остаток от деления в PythonКак найти следующее число в PythonИспользование Unicode в PythonТип int в Python и его особенностиИндекс списка в PythonФункции для работы со строками в PythonЭлементы Python и способы доступа к нимДоступ к элементам массива в PythonДеление чисел в PythonРабота с данными в Python на практикеКак работать с числами в Python
Открыть базу знаний

Лучшие курсы по теме

Иконка молнииНовый
изображение курса

Основы Python

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Nest.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.6
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Docker и Ansible

Антон Ларичев
AI-тренажерыAI-тренажеры
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий