为什么内容管理员不会重新提起例外?

时间:2016-02-16 15:14:37

标签: python python-3.x

我想用__enter__装饰的单个函数替换类的__exit__ / contextlib.contextmanager函数,这里是代码:

class Test:
    def __enter__(self):
        self._cm_obj = self._cm()
        self._cm_obj.__enter__()

    def __exit__(self, exc_type, exc_val, exc_tb):
        try:
            # Here we first time caught exception,
            # Pass it to _cm:
            self._cm_obj.__exit__(exc_type, exc_val, exc_tb)
        except:
            # Here we should catch exception reraised by _cm,
            # but it doesn't happen.
            raise
        else:
            return True

    @contextmanager
    def _cm(self):
        print('enter')
        try:
            yield
        except:
            # Here we got exception from __exit__
            # Reraise it to tell __exit__ it should be raised.
            raise
        finally:
            print('exit')


with Test():
    raise Exception(123)

当我们在Test __exit__中获得异常时,我们将其传递给_cm的{​​{1}},它工作正常,我在__exit__内看到异常。但是,当我决定重新加入_cm时,它不会发生:在Test _cm之后我没有看到异常(并且代码工作错误,无例外)。

为什么不在__exit__内重新加注exeprion?

如果这是正常行为,您能否建议我使用__exit__函数正确替换__enter__ / __exit__的解决方案?

修改

如果异常被抑制(或根本没有被提升),

contextmanager会在cm_obj.__exit__None内的异常上返回_cm

另一方面,在True内,我们可以返回Test.__exit__来传播当前例外,或None来抑制它。

看起来只是在True内部返回cm_obj.__exit__的值,这段代码按我的意愿运行:

Test.__exit__

2 个答案:

答案 0 :(得分:2)

__exit__中没有引发的异常,因此没有任何内容可以重新加注。

异常作为参数传递给方法,因为您不能在另一个方法(生成器除外)中引发异常。您也不必提出异常,只需返回而不是为要传播的异常返回true值。返回None就足够了。

来自With Statement Context Managers documentation

  

退出与此对象相关的运行时上下文。参数描述导致退出上下文的异常。如果上下文没有例外退出,则所有三个参数都是None

     

如果提供了异常,并且该方法希望抑制异常(即,防止它被传播),则它应该返回一个真值。否则,异常将在退出此方法时正常处理。

     

请注意,__exit__()方法不应该重新引用传入的异常;这是来电者的责任。

大胆强调我的。

您反而取消self._cm_obj.__exit__()调用的结果,并返回True以确保不再引发异常:

def __exit__(self, exc_type, exc_val, exc_tb):
    self._cm_obj.__exit__(exc_type, exc_val, exc_tb)
    return True

如果您未在此处返回True,则会看到with声明重新引发的异常。

但是,您无法重新引发未到达上下文管理器的异常。如果你在代码块中捕获并处理异常(例如在_cm内),那么上下文管理器将永远不会被告知这一点。被抑制的异常保持被抑制。

请注意,@contextmanager无法更改这些规则;虽然它使用generator.throw()将异常传输到生成器中,但如果生成器没有处理它,它也必须捕获相同的异常。它会在那一刻返回一个假值,因为contextmanager.__exit__()方法应该做的

答案 1 :(得分:1)

当上下文管理器的__exit__方法需要指示应该传播异常时,它不会通过从__exit__传播异常来实现。它通过返回一个假值来实现。

@contextmanager为您提供了不同的API,但它仍然需要将其转换为正常的上下文管理器API以显示给Python。当传递给@contextmanager上下文管理器的__exit__的异常传播出已包含的未被捕获的函数时,@contextmanager会捕获该异常并从__exit__返回一个假值。这意味着这个电话:

self._cm_obj.__exit__(exc_type, exc_val, exc_tb)

不会引发异常,因为它被@contextmanager抓住了。

您可以在Lib/contextlib.py

中看到@contextmanager捕获异常的代码
            try:
                self.gen.throw(type, value, traceback)
                raise RuntimeError("generator didn't stop after throw()")
            except StopIteration, exc:
                # Suppress the exception *unless* it's the same exception that
                # was passed to throw().  This prevents a StopIteration
                # raised inside the "with" statement from being suppressed
                return exc is not value
            except:
                # only re-raise if it's *not* the exception that was
                # passed to throw(), because __exit__() must not raise
                # an exception unless __exit__() itself failed.  But throw()
                # has to raise the exception to signal propagation, so this
                # fixes the impedance mismatch between the throw() protocol
                # and the __exit__() protocol.
                #
                if sys.exc_info()[1] is not value:
                    raise