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



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



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


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



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



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 года post , который использует 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__/__exit__ о его детях?
458   5  

5 ответов:

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

Оригинальный запрос на улучшение Python

Https://bugs.python.org/issue13585

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

Никрацио первоначально предложил:

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

Который был встречен с беспокойством со стороны реттингера:

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

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

Примера.setUp () и TestCase.демонтаж() был одним из предвестников__ввести__() и Выход(). addCleanUp() выполняет здесь точно такую же роль - и я виделмножество положительных отзывов, направленных на Майкла за это дополнение к unittest API... ...Пользовательские контекстные менеджеры обычно плохая идея в этих обстоятельствах, потому что они делают читаемость хуже (полагаясь на людей, чтобы понять, что context manager делает). Стандартное библиотечное решение, с другой стороны, предлагает лучшее из обоих миров: - код становится легче писать правильно и проверять на корректность (по всем причинам с утверждениями добавились в первую очередь) - идиома со временем станет знакомой всем пользователям 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-beauty-of-pythons-exitstack.html

Ваш второй пример-самый прямой способ сделать это в На Python (т. е. наиболее подходящие для Python). Однако в вашем примере все еще есть ошибка. Если исключение возникает во время второй 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__() успешно возвращается. Чтобы исправить это, оберните каждый вызов контекста в попробуйте-за исключением того, что вызовет 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

Как упоминал @cpburnz, ваш последний пример лучше, но содержит ошибку, если второй открытый не удается. Устранение этой ошибки описано в документации стандартной библиотеки. Мы можем легко адаптировать фрагменты кода из документации ExitStack и пример для ResourceManager из 29.6.2.4 очистки в реализации __enter__ , чтобы получить класс 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__ возвращает список объектов, заданных переданным acquire_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') непосредственно перед циклом, мы бы увидели:

/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 закрыт правильно.

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

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)

Использование близко к родному open:

with multiopen(i_name, o_name) as foo:
    pass

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

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 прост.

Comments

    Ничего не найдено.