Питонический способ создания контекстных менеджеров для объектов, принадлежащих классу

Типично требовать для некоторой задачи нескольких объектов, у которых есть ресурсы, которые должны быть явно выпущены - скажем, два файла; это легко сделать, когда задача является локальной для функции с использованием вложенных with блоками, или - еще лучше - один with блоком с несколькими with_item пунктов:

with open('in.txt', 'r') as i, open('out.txt', 'w') as o:
    # do stuff

OTOH, я все еще пытаюсь понять, как это должно работать, когда такие объекты не только локальны для области функций, но принадлежат экземпляру класса - другими словами, как создаются менеджеры контекста.

В идеале я хотел бы сделать что-то вроде:

class Foo:
    def __init__(self, in_file_name, out_file_name):
        self.i = WITH(open(in_file_name, 'r'))
        self.o = WITH(open(out_file_name, 'w'))

и Foo сам превращается в менеджер контекста, который обрабатывает i и o, так что когда я это делаю

with Foo('in.txt', 'out.txt') as f:
    # do stuff

self.i и self.o заботятся автоматически, как и следовало ожидать.

Я возился о написании таких вещей, как:

class Foo:
    def __init__(self, in_file_name, out_file_name):
        self.i = open(in_file_name, 'r').__enter__()
        self.o = open(out_file_name, 'w').__enter__()

    def __enter__(self):
        return self

    def __exit__(self, *exc):
        self.i.__exit__(*exc)
        self.o.__exit__(*exc)

но он как подробный, так и небезопасный в отношении исключений, возникающих в конструкторе. После некоторого времени поиска я нашел этот блог-блог в 2015 году, в котором используется contextlib.ExitStack чтобы получить что-то очень похожее на то, что мне нужно:

class Foo(contextlib.ExitStack):
    def __init__(self, in_file_name, out_file_name):
        super().__init__()
        self.in_file_name = in_file_name
        self.out_file_name = out_file_name

    def __enter__(self):
        super().__enter__()
        self.i = self.enter_context(open(self.in_file_name, 'r')
        self.o = self.enter_context(open(self.out_file_name, 'w')
        return self

Это довольно удовлетворительно, но я озадачен тем, что:

  • Я ничего не знаю об этом использовании в документации, поэтому он, похоже, не является "официальным" способом решения этой проблемы;
  • в общем, мне очень сложно найти информацию об этой проблеме, что заставляет меня думать, что я пытаюсь применить непитологическое решение проблемы.

Некоторый дополнительный контекст: я работаю в основном в C++, где нет разницы между случаем блочной области и случаем объектной области для этой проблемы, так как эта очистка реализована внутри деструктора (подумайте __del__, но вызывается детерминистически), а деструктор (даже если он явно не определен) автоматически вызывает деструкторы подобъектов. Итак, оба:

{
    std::ifstream i("in.txt");
    std::ofstream o("out.txt");
    // do stuff
}

а также

struct Foo {
    std::ifstream i;
    std::ofstream o;

    Foo(const char *in_file_name, const char *out_file_name) 
        : i(in_file_name), o(out_file_name) {}
}

{
    Foo f("in.txt", "out.txt");
}

сделайте всю очистку автоматически, как вы обычно этого хотите.

Я ищу аналогичное поведение в Python, но, опять же, я боюсь, что я просто пытаюсь применить шаблон, исходящий из C++, и что основная проблема имеет радикально другое решение, о котором я не могу думать,


Итак, чтобы подвести итог: что такое решение __enter__ с тем, что объект, которому принадлежат объекты, требующие очистки, становится самим менеджером контекста, правильно вызывая __enter__/__exit__ своих детей?

Ответ 1

Я думаю, что contextlib.ExitStack - это Pythonic и canonical, и это подходящее решение этой проблемы. Остальная часть этого ответа пытается показать ссылки, которые я использовал, чтобы прийти к такому выводу и моему мыслительному процессу:

Исходный запрос на улучшение Python

https://bugs.python.org/issue13585

Первоначальная идея + реализация была предложена как усовершенствование стандартной библиотеки Python с использованием как аргументации, так и примера кода. Это подробно обсуждалось такими основными разработчиками, как Раймонд Хеттингер и Эрик Сноу. Дискуссия по этому вопросу ясно показывает рост исходной идеи во что-то, что применимо к стандартной библиотеке и является Pythonic. Попытка суммирования потока:

Первоначально предлагалось предлагать nikratio:

Я хотел бы предложить добавить класс CleanupManager, описанный в http://article.gmane.org/gmane.comp.python.ideas/12447, в модуль contextlib. Идея состоит в том, чтобы добавить универсальный менеджер контекста для управления (python или non-python) ресурсами, которые не поставляются со своим собственным менеджером контекста

Что было встречено с озабоченностью геттингера:

До сих пор для этого был нулевой спрос, и я не видел кода, как он используется в дикой природе. AFAICT, это не намного лучше, чем прямолинейная попытка/наконец.

В ответ на это продолжилось обсуждение вопроса о том, есть ли необходимость в этом, приводя к подобным сообщениям из ncoghlan:

TestCase.setUp() и TestCase.tearDown() были среди предшественников to__enter __() и exit(). addCleanUp() заполняет ту же роль здесь - и я видел много положительных отзывов, направленных на Майкла для этого дополнения к API unittest...... Пользовательские контекстные менеджеры, как правило, плохая идея в этих обстоятельствах, потому что они делают читаемость хуже (полагаясь на людей, чтобы понять, что делает менеджер контекста). С другой стороны, стандартное решение на базе библиотеки предлагает лучшее из обоих миров: - код становится легче писать правильно и проверять правильность (по всем причинам с заявлениями были добавлены в первую очередь) - идиома в конечном итоге станет знакомый всем пользователям Python...... Я могу взять это на python-dev, если вы хотите, но я надеюсь убедить вас, что желание есть...

А потом снова из ncoghlan чуть позже:

Мои более ранние описания здесь не совсем адекватны - как только я начал создавать contextlib2 вместе, эта идея CleanupManager быстро превратилась в ContextStack [1], что является гораздо более мощным инструментом для управления контекстными менеджерами таким образом, который не обязательно соответствует с лексическим охватом в исходном коде.

Примеры/рецепты/сообщения в блоге из ExitStack В исходном коде стандартной библиотеки есть несколько примеров и рецептов, которые вы можете увидеть в версии слияния, которая добавила эту функцию: https://hg.python.org/cpython/rev/8ef66c73b1e1

Существует также сообщение в блоге от создателя оригинальной версии (Nikolaus Rath/nikratio), в котором убедительным образом объясняется, почему ContextStack является хорошим шаблоном, а также предоставляет некоторые примеры использования: https://www.rath.org/on-the- бьюти-оф-питонов-exitstack.html

Ответ 2

Ваш второй пример - самый прямой способ сделать это в Python (то есть, большинство Pythonic). Однако в вашем примере все еще есть ошибка. Если во время второго open() возникает исключение,

self.i = self.enter_context(open(self.in_file_name, 'r')
self.o = self.enter_context(open(self.out_file_name, 'w') # <<< HERE

то self.i не будет выпущен, когда вы ожидаете, потому что Foo.__exit__() не будет вызываться, если Foo.__enter__() успешно возвращен. Чтобы исправить это, оберните каждый контекстный вызов в try-except, за исключением того, что вызывается Foo.__exit__() когда возникает исключение.

import contextlib
import sys

class Foo(contextlib.ExitStack):

    def __init__(self, in_file_name, out_file_name):
        super().__init__()
        self.in_file_name = in_file_name
        self.out_file_name = out_file_name

    def __enter__(self):
        super().__enter__()

        try:
            # Initialize sub-context objects that could raise exceptions here.
            self.i = self.enter_context(open(self.in_file_name, 'r'))
            self.o = self.enter_context(open(self.out_file_name, 'w'))

        except:
            if not self.__exit__(*sys.exc_info()):
                raise

        return self

Ответ 3

Как отметил @cpburnz, ваш последний пример лучше всего, но содержит ошибку, если второй открытый сбой. Избегание этой ошибки описано в стандартной библиотечной документации. Мы легко адаптируем фрагменты кода из документации ExitStack и пример для ResourceManager из 29.6.2.4. Очистка в реализации __enter__ чтобы MultiResourceManager класс MultiResourceManager:

from contextlib import contextmanager, ExitStack
class MultiResourceManager(ExitStack):
    def __init__(self, resources, acquire_resource, release_resource,
            check_resource_ok=None):
        super().__init__()
        self.acquire_resource = acquire_resource
        self.release_resource = release_resource
        if check_resource_ok is None:
            def check_resource_ok(resource):
                return True
        self.check_resource_ok = check_resource_ok
        self.resources = resources
        self.wrappers = []

    @contextmanager
    def _cleanup_on_error(self):
        with ExitStack() as stack:
            stack.push(self)
            yield
            # The validation check passed and didn't raise an exception
            # Accordingly, we want to keep the resource, and pass it
            # back to our caller
            stack.pop_all()

    def enter_context(self, resource):
        wrapped = super().enter_context(self.acquire_resource(resource))
        if not self.check_resource_ok(wrapped):
            msg = "Failed validation for {!r}"
            raise RuntimeError(msg.format(resource))
        return wrapped

    def __enter__(self):
        with self._cleanup_on_error():
            self.wrappers = [self.enter_context(r) for r in self.resources]
        return self.wrappers

    # NB: ExitStack.__exit__ is already correct

Теперь ваш класс Foo() тривиален:

import io
class Foo(MultiResourceManager):
    def __init__(self, *paths):
        super().__init__(paths, io.FileIO, io.FileIO.close)

Это хорошо, потому что нам не нужны никакие блоки исключений - вы, вероятно, используете только ContextManagers, чтобы избавиться от них в первую очередь!

Затем вы можете использовать его так, как вы хотели (обратите внимание на MultiResourceManager.__enter__ возвращает список объектов, заданных переданным методом получения_resource()):

if __name__ == '__main__':
    open('/tmp/a', 'w').close()
    open('/tmp/b', 'w').close()

    with Foo('/tmp/a', '/tmp/b') as (f1, f2):
        print('opened {0} and {1}'.format(f1.name, f2.name))

Мы можем заменить io.FileIO на debug_file как в следующем фрагменте, чтобы увидеть его в действии:

    class debug_file(io.FileIO):
        def __enter__(self):
            print('{0}: enter'.format(self.name))
            return super().__enter__()
        def __exit__(self, *exc_info):
            print('{0}: exit'.format(self.name))
            return super().__exit__(*exc_info)

Затем мы видим:

/tmp/a: enter
/tmp/b: enter
opened /tmp/a and /tmp/b
/tmp/b: exit
/tmp/a: exit

Если мы добавили import os; os.unlink('/tmp/b') import os; os.unlink('/tmp/b') непосредственно перед циклом, который мы увидим:

/tmp/a: enter
/tmp/a: exit
Traceback (most recent call last):
  File "t.py", line 58, in <module>
    with Foo('/tmp/a', '/tmp/b') as (f1, f2):
  File "t.py", line 46, in __enter__
    self.wrappers = [self.enter_context(r) for r in self.resources]
  File "t.py", line 46, in <listcomp>
    self.wrappers = [self.enter_context(r) for r in self.resources]
  File "t.py", line 38, in enter_context
    wrapped = super().enter_context(self.acquire_resource(resource))
FileNotFoundError: [Errno 2] No such file or directory: '/tmp/b'

Вы можете видеть, что /tmp/a закрыто правильно.

Ответ 4

Я думаю, лучше использовать помощника:

from contextlib import ExitStack, contextmanager

class Foo:
    def __init__(self, i, o):
        self.i = i
        self.o = o

@contextmanager
def multiopen(i, o):
    with ExitStack() as stack:
        i = stack.enter_context(open(i))
        o = stack.enter_context(open(o))
        yield Foo(i, o)

Использование близко к native open:

with multiopen(i_name, o_name) as foo:
    pass

Ответ 5

Ну, если вы хотите обработать файлы для обработчиков файлов, самым простым решением является просто передать обработчики файлов непосредственно в ваш класс вместо имен файлов.

with open(f1, 'r') as f1, open(f2, 'w') as f2:
   with MyClass(f1, f2) as my_obj:
       ...

Если вам не нужны пользовательские функции __exit__ вы можете даже пропустить вложенные.

Если вы действительно хотите передать имена файлов __init__, ваша проблема может быть решена следующим образом:

class MyClass:
     input, output = None, None

     def __init__(self, input, output):
         try:
             self.input = open(input, 'r')
             self.output = open(output, 'w')
         except BaseException as exc:
             self.__exit___(type(exc), exc, exc.__traceback__)
             raise

     def __enter__(self):
         return self

     def __exit__(self, *args):
            self.input and self.input.close()
            self.output and self.output.close()
        # My custom __exit__ code

Таким образом, это действительно зависит от вашей задачи, у python есть много возможностей для работы. В конце дня - питоновский способ - держать ваш api простым.