Питонический способ создания контекстных менеджеров для объектов, принадлежащих классу
Типично требовать для некоторой задачи нескольких объектов, у которых есть ресурсы, которые должны быть явно выпущены - скажем, два файла; это легко сделать, когда задача является локальной для функции с использованием вложенных 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 простым.