【问题标题】:Catching exception in context manager __enter__()在上下文管理器 __enter__() 中捕获异常
【发布时间】:2023-03-26 06:27:01
【问题描述】:

即使__enter__()有异常,是否也能保证调用__exit__()方法?

>>> class TstContx(object):
...    def __enter__(self):
...        raise Exception('Oops in __enter__')
...
...    def __exit__(self, e_typ, e_val, trcbak):
...        print "This isn't running"
... 
>>> with TstContx():
...     pass
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __enter__
Exception: Oops in __enter__
>>> 

编辑

这是我能得到的最接近...

class TstContx(object):
    def __enter__(self):
        try:
            # __enter__ code
        except Exception as e
            self.init_exc = e

        return self

    def __exit__(self, e_typ, e_val, trcbak):
        if all((e_typ, e_val, trcbak)):
            raise e_typ, e_val, trcbak

        # __exit__ code


with TstContx() as tc:
    if hasattr(tc, 'init_exc'): raise tc.init_exc

    # code in context

事后看来,上下文管理器可能不是最好的设计决策

【问题讨论】:

  • 问题是,不能从__enter__ 中跳过with 正文(见pep 377

标签: python exception python-2.7 with-statement contextmanager


【解决方案1】:

像这样:

import sys

class Context(object):
    def __enter__(self):
        try:
            raise Exception("Oops in __enter__")
        except:
            # Swallow exception if __exit__ returns a True value
            if self.__exit__(*sys.exc_info()):
                pass
            else:
                raise


    def __exit__(self, e_typ, e_val, trcbak):
        print "Now it's running"


with Context():
    pass

要让程序继续其愉快的方式而不执行上下文块,您需要检查上下文块内的上下文对象,并且只有在 __enter__ 成功时才执行重要的操作。

class Context(object):
    def __init__(self):
        self.enter_ok = True

    def __enter__(self):
        try:
            raise Exception("Oops in __enter__")
        except:
            if self.__exit__(*sys.exc_info()):
                self.enter_ok = False
            else:
                raise
        return self

    def __exit__(self, e_typ, e_val, trcbak):
        print "Now this runs twice"
        return True


with Context() as c:
    if c.enter_ok:
        print "Only runs if enter succeeded"

print "Execution continues"

据我所知,您不能完全跳过 with 块。请注意,此上下文现在将 all 异常包含在其中。如果您不想在__enter__ 成功时吞下异常,请检查__exit__return False 中的self.enter_ok,如果它是True

【讨论】:

  • 如果__enter__有异常,你调用__exit__,有什么办法可以突破客户端代码中的with块?
  • 大声笑我只是同时想到了这一点。我用同样的逻辑更新了我的问题。
  • 如果目标是执行 exit 但不执行 with 内容,你不会只执行self.__exit__(*sys.exc_info()) 然后raise 原始异常不管退出 返回值?我错过了什么吗?
【解决方案2】:

没有。如果__enter__() 中可能发生异常,那么您需要自己捕获它并调用包含清理代码的辅助函数。

【讨论】:

    【解决方案3】:

    我建议您遵循 RAII(资源获取是初始化)并使用上下文的构造函数来执行可能失败的分配。然后你的 __enter__ 可以简单地返回 self ,它永远不会引发异常。如果您的构造函数失败,则可能在进入 with 上下文之前就抛出异常。

    class Foo:
        def __init__(self):
            print("init")
            raise Exception("booh")
    
        def __enter__(self):
            print("enter")
            return self
    
        def __exit__(self, exc_type, exc_val, exc_tb):
            print("exit")
            return False
    
    
    with Foo() as f:
        print("within with")
    

    输出:

    init
    Traceback (most recent call last):
      File "<input>", line 1, in <module>
      ...
        raise Exception("booh")
    Exception: booh
    

    编辑: 不幸的是,这种方法仍然允许用户创建“悬空”资源,如果他执行以下操作则不会被清理:

    foo = Foo() # this allocates resource without a with context.
    raise ValueError("bla") # foo.__exit__() will never be called.
    

    我很好奇这是否可以通过修改类的 new 实现或其他一些禁止在没有 with 上下文的情况下实例化对象的 python 魔法来解决。

    【讨论】:

    • +1:这个答案最适合我的用例,并且很有意义。很惊讶它没有更多的赞成票。谢谢。
    • 好吧,从技术上讲,我没有回答这个问题;-)
    【解决方案4】:

    您可以使用contextlib.ExitStack(未测试):

    with ExitStack() as stack:
        cm = TstContx()
        stack.push(cm) # ensure __exit__ is called
        with ctx:
             stack.pop_all() # __enter__ succeeded, don't call __exit__ callback
    

    或者来自the docs的例子:

    stack = ExitStack()
    try:
        x = stack.enter_context(cm)
    except Exception:
        # handle __enter__ exception
    else:
        with stack:
            # Handle normal case
    

    contextlib2 on Python <3.3

    【讨论】:

      【解决方案5】:

      如果不需要继承或复杂的子程序,可以使用更短的方式:

      from contextlib import contextmanager
      
      @contextmanager
      def test_cm():
          try:
              # dangerous code
              yield  
          except Exception, err
              pass # do something
      

      【讨论】:

      • 是的,但这会在 contextlib 中抛出“generator didn't yield”。
      • @thg435,合理,但我们可以尝试将“yield”包装起来......终于
      • 在 finally 块中是 yield
      • 有很多变通方法,但问题的根源在于无法跳过整个with 块。因此,即使我们设法以某种方式处理 enter 中的异常,该块仍将运行,并以 None 或其他一些垃圾作为参数。
      • 这在 Python 3.9 中不起作用 - 很高兴看到更新。
      【解决方案6】:
      class MyContext:
          def __enter__(self):
              try:
                  pass
                  # exception-raising code
              except Exception as e:
                  self.__exit__(e)
      
          def __exit__(self, *args):
              # clean up code ...
              if args[0]:
                  raise
      

      我已经这样做了。它以错误为参数调用 __exit__()。如果 args[0] 包含错误,它会在执行清理代码后重新引发异常。

      【讨论】:

        【解决方案7】:

        The docs 包含一个使用contextlib.ExitStack 确保清理的示例:

        ExitStack.push() 的文档中所述,如果__enter__() 实现中的后续步骤失败,此方法可用于清理已分配的资源。

        因此,您将使用 ExitStack() 作为围绕 TstContx() 上下文管理器的包装上下文管理器:

        from contextlib import ExitStack
        
        with ExitStack() as stack:
            ctx = TstContx()
            stack.push(ctx)  # Leaving `stack` now ensures that `ctx.__exit__` gets called.
            with ctx:
                stack.pop_all()  # Since `ctx.__enter__` didn't raise it can handle the cleanup itself.
                ...  # Here goes the body of the actual context manager.
        

        【讨论】:

          猜你喜欢
          • 2018-05-22
          • 2015-05-29
          • 1970-01-01
          • 2023-03-23
          • 1970-01-01
          • 2017-01-29
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多