区分复合with语句引发的异常的可能来源
区分with语句中出现的异常非常棘手,因为它们可能起源于不同的地方。可以从以下位置(或其中调用的函数)引发异常:
ContextManager.__init__
ContextManager.__enter__
身体的配合
ContextManager.__exit__
有关更多详细信息,请参阅关于上下文管理器类型的文档。
如果我们想要区分这些不同的情况,只需将with包装成一个try ..“除了”是不够的。考虑以下示例(使用ValueError作为示例,但当然它可以用任何其他异常类型代替):
try:
with ContextManager():
BLOCK
except ValueError as err:
print(err)
在这里,except将捕获来自所有四个不同位置的异常,因此不允许对它们进行区分。如果我们将上下文管理器对象的实例化移到with对象之外,我们可以区分__init__和BLOCK / __enter__ / __exit__:
try:
mgr = ContextManager()
except ValueError as err:
print('__init__ raised:', err)
else:
try:
with mgr:
try:
BLOCK
except TypeError: # catching another type (which we want to handle here)
pass
except ValueError as err:
# At this point we still cannot distinguish between exceptions raised from
# __enter__, BLOCK, __exit__ (also BLOCK since we didn't catch ValueError in the body)
pass
实际上,这只是帮助了__init__部分,但我们可以添加一个额外的哨兵变量来检查with对象的主体是否已经开始执行(即区分__enter__和其他变量):
try:
mgr = ContextManager() # __init__ could raise
except ValueError as err:
print('__init__ raised:', err)
else:
try:
entered_body = False
with mgr:
entered_body = True # __enter__ did not raise at this point
try:
BLOCK
except TypeError: # catching another type (which we want to handle here)
pass
except ValueError as err:
if not entered_body:
print('__enter__ raised:', err)
else:
# At this point we know the exception came either from BLOCK or from __exit__
pass
The tricky part is to differentiate between exceptions originating from BLOCK and __exit__ because an exception that escapes the body of the with will be passed to __exit__ which can decide how to handle it (see the docs). If however __exit__ raises itself, the original exception will be replaced by the new one. To deal with these cases we can add a general except clause in the body of the with to store any potential exception that would have otherwise escaped unnoticed and compare it with the one caught in the outermost except later on - if they are the same this means the origin was BLOCK or otherwise it was __exit__ (in case __exit__ suppresses the exception by returning a true value the outermost except will simply not be executed).
try:
mgr = ContextManager() # __init__ could raise
except ValueError as err:
print('__init__ raised:', err)
else:
entered_body = exc_escaped_from_body = False
try:
with mgr:
entered_body = True # __enter__ did not raise at this point
try:
BLOCK
except TypeError: # catching another type (which we want to handle here)
pass
except Exception as err: # this exception would normally escape without notice
# we store this exception to check in the outer `except` clause
# whether it is the same (otherwise it comes from __exit__)
exc_escaped_from_body = err
raise # re-raise since we didn't intend to handle it, just needed to store it
except ValueError as err:
if not entered_body:
print('__enter__ raised:', err)
elif err is exc_escaped_from_body:
print('BLOCK raised:', err)
else:
print('__exit__ raised:', err)
使用PEP 343中提到的等效形式的替代方法
PEP 343——“with”语句指定with语句的等效“非with”版本。在这里,我们可以很容易地用try…除了,从而区分不同的潜在错误来源:
import sys
try:
mgr = ContextManager()
except ValueError as err:
print('__init__ raised:', err)
else:
try:
value = type(mgr).__enter__(mgr)
except ValueError as err:
print('__enter__ raised:', err)
else:
exit = type(mgr).__exit__
exc = True
try:
try:
BLOCK
except TypeError:
pass
except:
exc = False
try:
exit_val = exit(mgr, *sys.exc_info())
except ValueError as err:
print('__exit__ raised:', err)
else:
if not exit_val:
raise
except ValueError as err:
print('BLOCK raised:', err)
finally:
if exc:
try:
exit(mgr, None, None, None)
except ValueError as err:
print('__exit__ raised:', err)
通常一个简单的方法就可以了
这种特殊异常处理的需要应该是相当罕见的,通常在一个try…Except块就足够了。特别是当各种错误源由不同的(自定义的)异常类型表示时(上下文管理器需要相应地设计),我们可以很容易地区分它们。例如:
try:
with ContextManager():
BLOCK
except InitError: # raised from __init__
...
except AcquireResourceError: # raised from __enter__
...
except ValueError: # raised from BLOCK
...
except ReleaseResourceError: # raised from __exit__
...