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__ о его детях? 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