对于某些任务,通常需要多个对象,这些对象具有要显式释放的资源-例如,两个文件;当任务对于使用嵌套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)
但是对于构造函数中发生的异常既冗长又不安全。搜索了一会后,我发现this 2015 blog 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 ++的模式,而根本的问题有一个根本不同的解决方案,我认为的。
因此,总而言之:让拥有需要清除的对象的对象成为上下文管理器本身的问题的Python解决方案是什么,正确地调用了__enter__
/ __exit__
它的孩子们?
答案 0 :(得分:7)
我认为contextlib.ExitStack是Pythonic的和规范的,它是解决此问题的合适方法。该答案的其余部分试图显示我用来得出该结论和思想过程的链接:
原始Python增强请求
https://bugs.python.org/issue13585
最初的想法+实现是作为Python标准库的增强而提出的,具有推理和示例代码。 Raymond Hettinger和Eric Snow等核心开发人员对此进行了详细讨论。关于这个问题的讨论清楚地表明了最初的想法已经发展成为适用于标准库并且是Pythonic的东西。线程的摘要尝试为:
nikratio最初建议:
我想建议将http://article.gmane.org/gmane.comp.python.ideas/12447中描述的CleanupManager类添加到contextlib模块中。这个想法是添加一个通用的上下文管理器来管理(不是Python或非python)自己的上下文管理器所没有的资源
遇到了来自瑞丁格的担忧:
到目前为止,对此的需求为零,我还没有看到像在野外那样使用的代码。 AFAICT,并没有比直接尝试/最终更好。
对此,人们进行了长时间的讨论,讨论是否需要这样做,导致来自ncoghlan的此类帖子:
TestCase.setUp()和TestCase.tearDown()属于__enter __()和 exit ()的前身。 addCleanUp()在这里扮演着完全相同的角色-我已经看到大量的积极反馈,针对Michael对该单元测试API的添加... 在这些情况下,自定义上下文管理器通常不是一个好主意,因为它们使可读性变得更差(依靠人们来理解上下文管理器的作用)。另一方面,基于标准库的解决方案提供了两全其美的优势: -代码变得更容易正确编写和审核正确性(由于所有原因,首先添加了带语句的原因) -这个成语最终将为所有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
答案 1 :(得分:6)
您的第二个示例是最直接的方法
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
答案 2 :(得分:6)
如@cpburnz所述,您的最后一个示例是最好的,但是如果第二次打开失败,则确实包含一个错误。标准库文档中描述了避免此错误的方法。我们可以轻松地改编ExitStack documentation的代码片段和29.6.2.4 Cleaning up in an __enter__
implementation的ResourceManager
的示例,以提供一个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)
这很好,因为我们不需要任何try-except块-您可能只在使用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已正确关闭。
答案 3 :(得分: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)
用法接近本地open
:
with multiopen(i_name, o_name) as foo:
pass
答案 4 :(得分:3)
好吧,如果您想对文件处理程序进行一定的处理,最简单的解决方案就是将文件处理程序直接传递给类而不是文件名。
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有很多选项可以使用。归根结底,Python方式是使您的api保持简单。