在上下文管理器__enter__()中捕获异常

问题描述 投票:0回答:8

即使

__exit__()
出现异常,是否也能保证调用
__enter__()
方法?

>>> 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

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

python exception python-2.7 with-statement contextmanager
8个回答
31
投票

像这样:

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 块。请注意,此上下文现在包含了其中的“所有”异常。如果您不想在 __enter__ 成功时吞下异常,请检查

self.enter_ok
中的
__exit__
return False
(如果是
True
)。
    


13
投票
__enter__()

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

    


5
投票
__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

实现或其他一些禁止在没有上下文的情况下实例化对象的Python魔法来解决这个问题。


4
投票
文档

包含一个使用 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.



3
投票
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

或者来自
文档

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

请参阅 Python 上的 
contextlib2

<3.3


3
投票

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



3
投票
我就是这样做的。它以错误作为参数调用 __exit__()。如果 args[0] 包含错误,它将在执行清理代码后重新引发异常。


0
投票

按照设计,采用 Python 编程语言,

__enter__函数抛出错误时,意味着未获取到要获取的资源,因此没有理由调用

__exit__

问题中没有解释在哪种情况下你想调用
__exit__

,但最常见的情况是

创建一个处理两个资源的上下文管理器
--- 换句话说,组合多个上下文生成器 对此,我认为最简单的解决方案是使用

@contextmanager

和一个函数 --- 事实上,我什至不知道如何正确编写一个类。

@contextmanager
def nest_resource(a, b):
    with a as aa, b as bb:
        yield (aa, bb)

如果以上情况不适合您,
您可以使用文档中的配方 

__enter__ 实现

中进行清理。
重要的部分是:

class ResourceManager: @contextmanager def _cleanup_on_error(self): with ExitStack() as stack: stack.push(self) yield stack.pop_all() def __enter__(self): resource = self.acquire_resource() with self._cleanup_on_error(): if not self.check_resource_ok(resource): raise RuntimeError(msg.format(resource)) return resource def __exit__(self, *exc_details): self.release_resource()

它的工作原理是:

如果
    acquire_resource
  • 发生错误,则
    __exit__
    不会
    被调用。 如果
  • check_resource_ok
  • 中发生错误(或者该函数返回
    False
    ),则将调用
    __exit__
    如果
  • __enter__
  • 平安归来,那么
    __exit__
    就不会被召唤。
    
    
  • 如果您想让
__exit__

__enter__
失败时始终被调用,只需在
with
块之前不放置任何内容即可。
    

© www.soinside.com 2019 - 2024. All rights reserved.