Контекстні менеджери
📌 Посилання на тему
- Python documentation - Context Manager Types
- Python documentation - 3.3.9. With Statement Context Managers
- Python documentation - The with statement
- Python documentation - contextlib — Utilities for with-statement contexts
📌 Визначення
Контекстні менеджери в Python — це дуже корисна концепція, яка дозволяє автоматично керувати ресурсами, такими як
- Файли: open()
- Блокування потоків: -
with lock
- Бази даних: відкриття/закриття транзакцій -
with connection
- Тимчасова зміна налаштувань (наприклад, зміна робочої директорії)
- Мережеві з’єднання
- Автоматичне очищення ресурсів
Контекстний менеджер — це об’єкт, який реалізує методи __enter__()
та __exit__()
. Цей об’єкт визначає що треба зробити:
-
при вході в контекст (наприклад, відкрити файл, створити з’єднання, взяти блокування);
-
при виході з контексту (закрити файл, відпустити з’єднання, зняти блокування), навіть якщо сталася помилка.
Щоб бути контекстним менеджером, об’єкт повинен мати два методи:
-
__enter__(self)
— викликається при вході в контекст (після with); -
__exit__(self, exc_type, exc_value, traceback)
— викликається при виході (навіть у разі помилки).exc_type, exc_value, traceback
- параметри, через які передаються дані про виключення, в разі його виникнення- якщо виключення не сталось, передаються None
- Якщо цей метод повертає True, то у випадку коли у вкладеному блоці коду у структурі
with
виникає виключення, то воно не зупиняє виконання програми, але дані про виключення все одно передаються в метод__exit__()
Порядок роботи з with
виразом
with A() as a, B() as b:
suite
A()
- виклик контекстного менеджераas a
- результат вмконання присвоюється вa
A() as a, B() as b
- в одному рядку можна викликати декілька контекстних менеджерів
Альтернативний варіант для роботи з декількома контекстними менеджерами:
with A() as a:
with B() as b:
suite
Послідовність виконання методу with
для одного контекстного менеджера:
- Отримання об'єкта контекстного менеджера.
- Завантаження магічного метода
__exit__
цього контекстного менеджера, для майбутнього автоматичного використання при виході з виразуwith
. - Виконується магічний метод
__enter__
(об'єкта контекстного менеджера). - Якщо він повертає якесь значення, то воно присвоюється у вираз
as
.якщо метод
__enter__
виконався без помилки, то метод__exit__
, прив'язаний до контекстного менеджера виконається при будь яких обставинах. Навіть якщо в середині блоку коду, що вкладено післяwith
, виникне помилка. - Виконання виконання вкладеного блоку коду
suite
. - Виконання методу
__exit__
Якщо блок коду
suite
видає помилку, то її тип, значення і повний трейсбек передаються у метод__exit__
Зазвичай менеджери контексту викликаються за допомогою інструкції with, , яка гарантує, що ресурси будуть закриті або звільнені, навіть якщо виникла помилка. Але їх також можна використовувати шляхом прямого виклику їхніх методів (__enter__
, __exit__
).
Використання контекстного менеджера через with
:
with open("data.txt", "r") as f:
content = f.read()
# Тут файл автоматично закрито, навіть якщо всередині було виключення
Приклад використання контекстного менеджера без with
:
cm = open("data.txt", "w") # створили контекстний менеджер
try:
cm.__enter__() # вхід у контекст
cm.write("Привіт без with!\n")
except Exception as e:
# Передаємо інформацію про помилку в __exit__
import sys
exc_info = sys.exc_info() # (type, value, traceback)
cm.__exit__(*exc_info)
else:
cm.__exit__(None, None, None) # вихід із контексту
Внутрішній механізм with
Під капотом конструкція
with MyCM() as val:
suite
еквівалентна:
cm = MyCM()
val = cm.__enter__()
try:
suite
except Exception as e:
cm.__exit__(type(e), e, e.__traceback__)
raise
else:
cm.__exit__(None, None, None)
Тобто
__exit__
завжди викликається — навіть якщо у блоці була помилка.
📌 Створення власного контекстного менеджера
Реалізувати власний контекстний менеджер можна:
- Через класи
- Використанням декоратор
@contextmanager
із вбудованого модуляcontextlib
Класовий спосіб
class MyCM:
def __enter__(self):
print("Вхід у контекст")
return "Дані"
def __exit__(self, exc_type, exc_value, tb):
print("Вихід із контексту")
print("Тип помилки: ", exc_type)
print("Об'єкт помилки:", exc_value)
print("Traceback: ", tb)
# Якщо повернути True — помилка буде приглушена
return True
with MyCM() as data:
print('Working...')
print('Data = ', data)
🧾 Вивід
Вхід у контекст
Working...
Data = Дані
Вихід із контексту
Тип помилки: None
Об'єкт помилки: None
Traceback: None
Декоратор @contextmanager
із contextlib
from contextlib import contextmanager
@contextmanager
def my_context():
print("Входимо")
yield "Ресурс"
print("Виходимо")
with my_context() as res:
print("Всередині:", res)
- 🔧 yield розділяє вхід і вихід з контексту
- блок коду до yield є аналогічним реалізації методу
__enter__
- блок коду після виразу yield аналогічний реалізацї методу
__exit__
📌 Приклади
⏱ Контекстний менеджер для таймера
Цей менеджер вимірює, скільки часу займає виконання блоку коду.
import time
class Timer:
def __enter__(self):
self.start = time.time()
return self # можна повертати об'єкт, щоб використовувати його всередині
def __exit__(self, exc_type, exc_value, traceback):
self.end = time.time()
self.interval = self.end - self.start
print(f"Час виконання: {self.interval:.4f} секунд")
# Приклад використання
with Timer():
total = sum(range(1000000))
__enter__
зберігає час початку__exit__
обчислює час завершення і виводить результат
📋 Контекстний менеджер для логування
Цей менеджер записує повідомлення до лог-файлу при вході та виході з блоку.
from contextlib import contextmanager
@contextmanager
def log_action(action_name):
with open("log.txt", "a") as log_file:
log_file.write(f"[START] {action_name}\n")
try:
yield
finally:
log_file.write(f"[END] {action_name}\n")
# Приклад використання
with log_action("Обробка даних"):
print("Тут відбувається обробка...")
📁 У файл log.txt
буде записано:
[START] Обробка даних
[END] Обробка даних
📂 Контекстний менеджер: тимчасова зміна директорі
import os
class SafeChangeDirectory:
def __init__(self, new_path):
self.new_path = new_path
self.original_path = os.getcwd()
def __enter__(self):
if not os.path.exists(self.new_path):
print(f"Директорія '{self.new_path}' не існує. Створюю...")
os.makedirs(self.new_path)
else:
print(f"Директорія '{self.new_path}' вже існує.")
os.chdir(self.new_path)
print(f"Перехід до: {self.new_path}")
def __exit__(self, exc_type, exc_value, traceback):
os.chdir(self.original_path)
print(f"Повернення до: {self.original_path}")
✅ Приклад використання:
print("Поточна директорія:", os.getcwd())
with SafeChangeDirectory("test_folder"):
print("Всередині блоку:", os.getcwd())
print("Після блоку:", os.getcwd())