【问题标题】:Pythonic way to compose context managers for objects owned by a class为类拥有的对象组合上下文管理器的 Pythonic 方法
【发布时间】:2018-08-02 08:47:53
【问题描述】:

对于某些任务,通常需要多个具有显式释放资源的对象 - 例如,两个文件;当任务是使用嵌套 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 本身变成处理io 的上下文管理器,这样当我这样做时

with Foo('in.txt', 'out.txt') as f:
    # do stuff

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)

但是对于构造函数中发生的异常,它既冗长又不安全。找了一阵子,找到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

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

  • 我在文档中没有找到关于此用法的任何信息,因此它似乎不是解决此问题的“官方”方式;
  • 总的来说,我发现很难找到有关此问题的信息,这让我觉得我正在尝试对问题应用非 Python 的解决方案。

一些额外的上下文:我主要在 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__ 的问题,Pythonic 的解决方案是什么?

【问题讨论】:

  • 我会说 ExitStack 的解决方案非常 Pythonic。
  • @BrenBarn:很高兴知道,但我仍然有点害怕,因为我认为在随机博客中只提到了这个解决方案,而不是在官方文档中将是一个相当普遍的问题。这就是让我感到困惑的原因。
  • 我不知道你为什么期望它出现在官方文档中。一般来说,官方文档只记录事物的工作方式,而不是它们的用途。官方文档中没有解释大量常见问题的解决方案。 Here 是一个相关问题,在对他的回答的评论中,Martijn Pieters 还建议将 ExitStack 子类化以实现看起来相关的目的。
  • 一个上下文管理器,旨在简化以编程方式组合其他上下文管理器和清理功能,尤其是那些可选的或由输入数据驱动的那些。我觉得文档表明ExitStack 解决方案是完美的 Pythonic。

标签: python contextmanager


【解决方案1】:

我认为 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)没有自带上下文管理器的资源

这引起了 rhettinger 的关注:

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

作为对此的回应,人们就是否需要这样做进行了长时间的讨论,导致 ncoghlan 发布了这样的帖子:

TestCase.setUp() 和 TestCase.tearDown() 是 __enter__() 和 exit() 的前身。 addCleanUp() 在这里扮演完全相同的角色 - 我已经看到大量针对 Michael 的积极反馈,因为它添加到 unittest API... ...在这些情况下,自定义上下文管理器通常不是一个好主意,因为它们使可读性更糟(依赖于人们理解上下文管理器的作用)。另一方面,基于标准库的解决方案提供了两全其美: - 代码变得更容易正确编写和审核正确性(出于所有原因,首先添加了语句) - 这个成语最终会为所有 Python 用户所熟悉...... ...如果你愿意,我可以在 python-dev 上接受这个,但我希望能说服你的愿望......

稍后再从 ncoghlan:

我之前在这里的描述还不够充分——当我开始将 contextlib2 放在一起时,这个 CleanupManager 的想法很快就变成了 ContextStack [1],它是一个更强大的工具,可以以一种不t 必须与源代码中的词法范围相对应。

ExitStack 的示例/食谱/博客文章 标准库源代码本身中有几个示例和配方,您可以在添加此功能的合并修订中看到:https://hg.python.org/cpython/rev/8ef66c73b1e1

还有一篇来自原始问题创建者 (Nikolaus Rath / nikratio) 的博文以令人信服的方式描述了为什么 ContextStack 是一个好的模式并提供了一些使用示例:https://www.rath.org/on-the-beauty-of-pythons-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 documentation 的代码sn-ps 和来自29.6.2.4 Cleaning up in an __enter__ implementationResourceManager 的示例以提出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 块——您可能只是首先使用 ContextManager 来摆脱那些!

      然后你就可以随心所欲地使用它了(注意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,如下面的sn-p 所示:

          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 已正确关闭。

      【讨论】:

        【解决方案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
        

        【讨论】:

        • 您的第一个 sn-p 显示出与我在 我修改了诸如之类的东西之后写的相同的错误(如果我在执行第二个 @987654326 时遇到异常怎么办@?);还有,我为什么要记得close() 的东西?这只是要编写的样板代码,并且可能会出错;上下文管理器(和 C++ 中的析构函数)的重点是让它自动处理,无论我们如何退出上下文。
        • 我之前没有考虑过。我现在正在考虑。但是您使用contextlib.ExitStack 的示例仍然不是一个好主意,因为在这种情况下,用户必须使用with 语句,否则他们获得的实例具有完全不同的行为。对于close() 的东西,你的观点也是有道理的。但《Python 之禅》说“显式胜于隐式”。至少对我来说,我会明确地关闭底层对象,以便我完全知道我在做什么。当然也可以使用__enter__/__exit__ pair。
        • 可以通过将整个初始化移动到__init__而不是__enter__来轻松调整该示例;在这种情况下,即使“手动”使用它(创建和显式 close())也可以正常工作(example)。
        • 至于显式优于隐式,这完全取决于。资源清理是自动化程度越高越好的领域之一,因为开发人员在这方面出了名的糟糕,尤其是在有异常的语言中。 Python 没有自动内存管理的机会,即使“显式优于显式”。
        • 是的,这取决于。关于你的例子,我写了另一个example,我认为它更接近原生open并且更简单。
        【解决方案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 有很多选择。归根结底——pythonic 的方式是让你的 api 保持简单。

        【讨论】:

          猜你喜欢
          • 2015-09-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-01-18
          • 2015-08-07
          • 2018-05-23
          • 2021-05-24
          • 1970-01-01
          相关资源
          最近更新 更多