Pythonic为类所拥有的对象组成上下文管理器的方法

Mat*_*lia 23 python contextmanager

对于某些任务,通常需要多个具有明确释放资源的对象 - 例如,两个文件; 当任务是使用嵌套with块的函数的本地任务时,这很容易完成,或者 - 甚至更好 - with具有多个with_item子句的单个块:

with open('in.txt', 'r') as i, open('out.txt', 'w') as o:
    # do stuff
Run Code Online (Sandbox Code Playgroud)

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'))
Run Code Online (Sandbox Code Playgroud)

并有Foo自己变成上下文管理,处理io,这样,当我做

with Foo('in.txt', 'out.txt') as f:
    # do stuff
Run Code Online (Sandbox Code Playgroud)

self.iself.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)
Run Code Online (Sandbox Code Playgroud)

但是对于构造函数中发生的异常,它既冗长又不安全.在搜索了一段时间之后,我发现了这篇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
Run Code Online (Sandbox Code Playgroud)

这非常令人满意,但我对以下事实感到困惑:

  • 我在文档中没有发现这种用法,所以它似乎不是解决这个问题的"官方"方式;
  • 总的来说,我发现找到有关这个问题的信息非常困难,这让我觉得我正在尝试对问题应用unpythonic解决方案.

一些额外的上下文:我主要在C++中工作,在这个问题上,块范围情况和对象范围情况之间没有区别,因为这种清理是在析构函数中实现的(想想__del__,但是确定性地调用),并且析构函数(即使没有明确定义)会自动调用子对象的析构函数.所以两者:

{
    std::ifstream i("in.txt");
    std::ofstream o("out.txt");
    // do stuff
}
Run Code Online (Sandbox Code Playgroud)

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");
}
Run Code Online (Sandbox Code Playgroud)

按照您的需要自动完成所有清理工作.

我在Python中寻找类似的行为,但同样,我担心我只是尝试应用来自C++的模式,并且潜在的问题有一个根本不同的解决方案,我无法想到.


所以,总结一下:什么是Python的解决方案,其谁拥有那些需要清理对象的对象的问题成为一个上下文管理器本身,调用正确的__enter__/ __exit__它的孩子吗?

Mat*_*rst 8

我认为contextlib.ExitStack是Pythonic和canonical,它是解决这个问题的合适方法.这个答案的其余部分试图显示我用来得出这个结论和思考过程的链接:

原始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)资源,这些资源没有自己的上下文管理器

这遭到了rhettinger的关注:

到目前为止,对此的需求为零,我没有看到它在野外使用的代码.AFAICT,它并不比直接的尝试/最终更好.

作为对此的回应,对于是否需要这一点进行了长时间的讨论,从而导致ncoghlan这样的帖子:

TestCase.setUp()和TestCase.tearDown()是__()和exit()的前身之一.addCleanUp()在这里填充了完全相同的角色 - 我已经看到了大量针对迈克尔的积极反馈,因为它增加了单元测试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-美容的-蟒蛇,exitstack.html


Sra*_*raw 6

我认为使用助手更好:

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)
Run Code Online (Sandbox Code Playgroud)

用法接近原生open

with multiopen(i_name, o_name) as foo:
    pass
Run Code Online (Sandbox Code Playgroud)


use*_*450 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
Run Code Online (Sandbox Code Playgroud)

然后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
Run Code Online (Sandbox Code Playgroud)


fri*_*elr 6

正如@cpburnz所提到的,你的最后一个例子是最好的,但如果第二个打开失败则确实包含一个错误.标准库文档中描述了避免此错误.我们可以很容易地从ExitStack文档中调整代码片段,并ResourceManager29.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
Run Code Online (Sandbox Code Playgroud)

现在你的Foo()类是微不足道的:

import io
class Foo(MultiResourceManager):
    def __init__(self, *paths):
        super().__init__(paths, io.FileIO, io.FileIO.close)
Run Code Online (Sandbox Code Playgroud)

这很好,因为我们不需要任何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))
Run Code Online (Sandbox Code Playgroud)

我们可以更换io.FileIOdebug_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)
Run Code Online (Sandbox Code Playgroud)

然后我们看到:

/tmp/a: enter
/tmp/b: enter
opened /tmp/a and /tmp/b
/tmp/b: exit
/tmp/a: exit
Run Code Online (Sandbox Code Playgroud)

如果我们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'
Run Code Online (Sandbox Code Playgroud)

您可以看到/ tmp/a已正确关闭.