我想用__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__
答案 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
抓住了。
@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