为什么Context Manager不会重新引发异常?

4

我想用被contextlib.contextmanager修饰的单一函数来替代类中的__enter__/__exit__函数,下面是代码:

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的__exit__,它可以正常工作,我在_cm中看到了异常。但是,当我决定在_cm中重新引发异常时,它并没有发生:在Test的__exit__之后,我没有看到异常(代码运行不正确,没有异常)。
为什么在__exit__中不会重新引发异常?
如果这是正常行为,您能否建议我正确地使用contextmanager函数替换__enter__/__exit__的解决方案?
编辑:
cm_obj.__exit__在_cm内部发生异常时返回None,并在抑制异常(或根本未引发异常)时返回True。
另一方面,在Test.__exit__内部,我们可以返回None以传播当前异常,或返回True以抑制它。
看起来只需在Test.__exit__中返回cm_obj.__exit__的值即可完成工作,此代码按照我的要求工作:
class Test:
    def __enter__(self):
        self._cm_obj = self._cm()
        self._cm_obj.__enter__()

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

    @contextmanager
    def _cm(self):
        print('---- enter')
        try:
            yield
        except:
            raise  # comment to suppess exception
            pass
        finally:
            print('---- exit')


with Test():
    raise Exception(123)
2个回答

3
异常并不是在__exit__中被raise的,所以没有东西可以reraise。
异常作为参数传递给方法,因为你不能在另一个方法中(除了生成器)抛出异常。你也不必抛出异常,只需要简单地返回not返回真值即可传播异常。返回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()将异��传递到生成器中,但如果您的生成器未处理该异常,它也必须捕获相同的异常。此时,它将返回一个false值,因为这是一个contextmanager.__exit__()方法应该执行的操作


返回True以确保异常不再被引发 - 在这种情况下,每个异常都将被抑制,但我希望它仅在_cm()内部被抑制并且如果在_cm()内部未被抑制,则重新引发。 - Mikhail Gerasimov
@germn:你做不到。在块中被抑制的异常无法传递到上下文管理器。 - Martijn Pieters
1
你不能在另一个方法中(除了生成器之外的方法)引发异常,这也是@contextmanager要求你提供生成器的原因之一。 - user2357112
@user2357112: 是的,异常是使用generator.throw()返回的。但是contextmanager.__exit__不是一个生成器,因此在托管上下文中引发的异常不会在其中抛出。 - Martijn Pieters
@MartijnPieters 我编辑了帖子。你能否看一下,我写的是否正确? - Mikhail Gerasimov
@germn:是的,返回self._cm.__exit__()的结果意味着您正确地包装了上下文管理器。 - Martijn Pieters

2
当上下文管理器的 __exit__ 方法需要表明应该传播异常时,它不会通过将异常从 __exit__ 传播出去来实现。它是通过返回一个 falsy 值来实现的。
@contextmanager 为您提供了不同的 API,但它仍然需要将其转换为正常的上下文管理器 API 来显示给 Python。当传递给 @contextmanager 上下文管理器的 __exit__ 的异常未被捕获地传播出包装函数时,@contextmanager 捕获异常并从 __exit__ 返回一个 falsy 值。这意味着这个调用:
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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接