ExitStack в Python: как закрывать переменное число файлов

ExitStack в Python: как закрывать переменное число файлов
Содержание статьи

Если программа получает список файлов от пользователя, количество блоков with заранее неизвестно. В Python эту задачу решает contextlib.ExitStack: он собирает действия выхода и выполняет их при завершении своего блока. На Mentoring.digital покажем, как закрыть уже открытые файлы, даже если следующий открыть не удалось.

Когда обычного with уже неудобно

Для одного файла достаточно with open(...). Для двух известных ресурсов можно перечислить два контекстных менеджера в одном блоке. Но список из конфигурации или аргументов командной строки имеет переменную длину. Попытка сначала открыть всё обычным списковым выражением, а затем закрывать файлы вручную создаёт вопрос: кто освободит первые ресурсы, если ошибка возникнет посередине?

ExitStack позволяет регистрировать каждый успешно открытый ресурс сразу. Для небольшой фиксированной конструкции он обычно не нужен: выбирайте его там, где набор ресурсов определяется во время выполнения.

Открытие файлов и гарантированный выход

from contextlib import ExitStack
from pathlib import Path

def read_files(paths):
    with ExitStack() as stack:
        files = [
            stack.enter_context(
                Path(path).open(encoding="utf-8")
            )
            for path in paths
        ]
        return [file.read() for file in files]

enter_context() входит в контекст переданного объекта и регистрирует выход из него. Для файла возвращается открытый файловый объект. Если все открытия успешны, чтение выполняется внутри блока, а перед возвратом результата файлы закрываются.

Если открытие второго файла вызывает FileNotFoundError, выход из ExitStack всё равно закрывает первый. Исключение не превращается автоматически в успешный результат. Если по требованиям отсутствующие файлы нужно пропускать, это отдельное решение обработки ошибок, которое не следует прятать в управлении ресурсами.

Как проверить поведение при исключении

from contextlib import ExitStack
from tempfile import TemporaryDirectory
from pathlib import Path

with TemporaryDirectory() as directory:
    root = Path(directory)
    (root / "a.txt").write_text("A", encoding="utf-8")
    opened = []
    try:
        with ExitStack() as stack:
            for name in ["a.txt", "missing.txt"]:
                file = stack.enter_context(
                    (root / name).open(encoding="utf-8")
                )
                opened.append(file)
    except FileNotFoundError:
        pass
    assert len(opened) == 1
    assert opened[0].closed

Здесь ошибка ожидается только для демонстрации очистки. В прикладном коде её обычно передают выше или превращают в понятное сообщение, а не молча игнорируют. Проверка closed подтверждает освобождение ресурса, а не только наличие нужной строки в логе.

Callback и порядок очистки

Для обычной функции очистки используйте stack.callback(function, arguments). Зарегистрированные действия выполняются в обратном порядке: сначала последнее, затем предыдущее. Это удобно для зависимостей, когда более поздний ресурс опирается на более ранний.

Callback, добавленный этим методом, не получает сведения об исключении и не может подавить его. А контекстный менеджер, подключённый через enter_context(), сохраняет обычное поведение своего метода выхода. Не рассчитывайте и на сборщик мусора: ExitStack следует использовать в with либо явно закрывать.

Для проверки в собственном проекте начните с двух сценариев: успешное чтение и ошибка после первого открытого ресурса. Если архитектура обработки исключений запуталась, её можно разобрать с наставником по Python.

Похожие статьи по данной теме