区分从复合 with 语句引发的异常的可能来源
区分with 语句中发生的异常很棘手,因为它们可能源自不同的地方。可以从以下任一位置(或其中调用的函数)引发异常:
ContextManager.__init__
ContextManager.__enter__
-
with 的正文
ContextManager.__exit__
有关更多详细信息,请参阅有关 Context Manager Types 的文档。
如果我们想区分这些不同的情况,仅仅将with 包装成try .. except 是不够的。考虑以下示例(以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
棘手的部分是区分源自BLOCK 和__exit__ 的异常,因为逃脱with 主体的异常将被传递给__exit__,它可以决定如何处理它(参见@987654322 @)。但是,如果 __exit__ 引发自身,则原始异常将被新异常替换。为了处理这些情况,我们可以在with 的主体中添加一个通用的except 子句来存储任何可能会被忽视的异常,并将其与稍后在最外面的except 中捕获的异常进行比较 - 如果它们是相同的,这意味着来源是BLOCK,否则它是__exit__(如果__exit__通过返回一个真值来抑制异常,那么最外面的except将不会被执行)。
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 -- The "with" Statement 指定了with 语句的等效“non-with”版本。在这里,我们可以很容易地用try ... except 包裹各个部分,从而区分不同的潜在错误源:
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)
通常更简单的方法就可以了
这种特殊异常处理的需求应该很少见,通常将整个with 包装在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__
...