2.24. Контекстні менеджери

Оператор with виконує код ініціалізації, потім тіло, а потім код завершення – з гарантією, що код завершення виконається навіть якщо тіло завершиться з помилкою. Пара методів, що забезпечує ініціалізацію та завершення, називається контекстним менеджером.

Синтаксис:

with <expression> as <name>:
    <body>

Вираз повертає контекстний менеджер. Python викликає його метод __enter__, опціонально прив’язує результат до <name>, виконує тіло, а потім викликає __exit__ – незалежно від того, чи тіло завершилося нормально або викинуло виключення.

Flow diagram: __enter__ runs first, then the body; the body either completes normally or raises; either way, __exit__ runs at the end.

__exit__ виконується в кінці блоку незалежно від того, що відбувається всередині нього.

2.24.1. Використання контекстного менеджера

Класичний приклад — відкриття файлу:

with open("data.txt") as f:
    text = f.read()

# f is now closed, even if read() failed

open() повертає об’єкт файлу, який сам є контекстним менеджером; __enter__ повертає файл, а __exit__ закриває його. Блок with робить принцип «завжди закривати файл після роботи» типовою поведінкою, а не тим, про що має пам’ятати той, хто викликає.

2.24.1.1. Кілька контекстних менеджерів

Один оператор with може одночасно входити до кількох контекстних менеджерів, розділених комами:

with open("input.txt") as src, open("output.txt", "w") as dst:
    dst.write(src.read())

Еквівалентно двом вкладеним блокам with, але плоскіше. Менеджери входять зліва направо і виходять у зворотному порядку; якщо __enter__ правого менеджера викидає виключення, __exit__ лівого менеджера все одно виконається.

2.24.2. Написання контекстного менеджера

Будь-який клас з методами __enter__ і __exit__ може використовуватися як контекстний менеджер:

class Section:
    def __init__(self, label):
        self.label = label

    def __enter__(self):
        print("---", self.label, "---")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("--- end", self.label, "---")
        return False

with Section("setup"):
    print("doing the work")

Виведення:

--- setup ---
doing the work
--- end setup ---

__exit__ отримує три аргументи, що описують виключення, яке завершило блок, або три значення None, якщо блок завершився нормально. Повернення False (або None) дозволяє будь-якому виключенню поширюватися після завершення; повернення True поглинуло б його.

Використовуйте контекстні менеджери для будь-якого ресурсу, що має цикл «відкрити / закрити» або «захопити / звільнити» – не лише для файлів. Цей шаблон тримає завершення поруч з ініціалізацією в тому місці, де обидва представлені, тому забутий виклик close в середині довгої функції не може призвести до витоку ресурсу.