先前的错误被当前异常上下文掩盖
下面是我在Doug Hellman的网站上找到的一个例子,文件名叫“masking_exceptions_catch.py”。我现在找不到链接。这个例子中,throws()函数抛出的异常被忽略了,而cleanup()函数抛出的异常则被报告出来。
在他的文章中,Doug提到这种处理方式让人感觉不太直观。我一开始以为这可能是个bug,或者是当时Python版本的限制(大约在2009年)。于是我在当前的Mac版Python(2.7.6)上运行了这个例子,结果还是报告了来自cleanup()的异常。我觉得这有点不可思议,想看看这实际上是怎样的正确或理想的行为。
#!/usr/bin/env python
import sys
import traceback
def throws():
raise RuntimeError('error from throws')
def nested():
try:
throws()
except:
try:
cleanup()
except:
pass # ignore errors in cleanup
raise # we want to re-raise the original error
def cleanup():
raise RuntimeError('error from cleanup')
def main():
try:
nested()
return 0
except Exception, err:
traceback.print_exc()
return 1
if __name__ == '__main__':
sys.exit(main())
程序输出:
$ python masking_exceptions_catch.py
Traceback (most recent call last):
File "masking_exceptions_catch.py", line 24, in main
nested()
File "masking_exceptions_catch.py", line 14, in nested
cleanup()
File "masking_exceptions_catch.py", line 20, in cleanup
raise RuntimeError('error from cleanup')
RuntimeError: error from cleanup
1 个回答
回到之前的问题。我先不回答你的问题。:-)
这个真的有效吗?
def f():
try:
raise Exception('bananas!')
except:
pass
raise
那么,上面的代码到底做了什么呢?请准备好音乐。
好了,大家停止思考。
# python 3.3
4 except:
5 pass
----> 6 raise
7
RuntimeError: No active exception to reraise
# python 2.7
1 def f():
2 try:
----> 3 raise Exception('bananas!')
4 except:
5 pass
Exception: bananas!
嗯,这真是有收获。为了好玩,我们来给异常命名。
def f():
try:
raise Exception('bananas!')
except Exception as e:
pass
raise e
接下来怎么办?
# python 3.3
4 except Exception as e:
5 pass
----> 6 raise e
7
UnboundLocalError: local variable 'e' referenced before assignment
# python 2.7
4 except Exception as e:
5 pass
----> 6 raise e
7
Exception: bananas!
在Python 2和3之间,异常的处理方式发生了很大的变化。如果你觉得Python 2的行为让你感到惊讶,那就考虑一下:它基本上和Python在其他地方的行为是一致的。
try:
1/0
except Exception as e:
x=4
#can I access `x` here after the exception block? How about `e`?
try
和except
并不是作用域。实际上,在Python中,只有少数东西是作用域;我们有个“LEGB规则”来记住四个命名空间——局部、封闭、全局和内置。其他的代码块并不是作用域;我可以在for
循环中声明x
,并且在循环结束后仍然可以引用它。
所以,这就有点尴尬了。异常是否应该被特别处理,只在它们的封闭词法块内?Python 2说不,Python 3说是的。但我在这里简化了;你最初问的是裸raise
,这些问题是紧密相关的,但实际上并不完全相同。Python 3可以规定命名异常的作用域仅限于它们的块,而不涉及裸raise
的问题。
裸raise
到底做了什么‽
常见的用法是使用裸raise
来保留堆栈跟踪。捕获异常,进行日志记录/清理,然后重新抛出。很好,我的清理代码不会出现在堆栈跟踪中,这样99.9%的情况下都能正常工作。但是,当我们尝试在异常处理器中处理嵌套异常时,事情可能会变得复杂。有时候。(请查看底部的示例,了解何时会/不会出现问题)
直观上来说,没有参数的raise
应该能正确处理嵌套异常处理器,并找出正确的“当前”异常来重新抛出。但现实并非如此。实际上,异常信息作为当前帧对象的一个成员被保存。在Python 2中,没有机制来处理在单个帧内推入/弹出异常处理器;只有一个字段保存了最后一个异常,而不管我们对它做了什么处理。这就是裸raise
所获取的。
6.9. raise语句
raise_stmt ::= "raise" [expression ["," expression ["," expression]]]
如果没有表达式,
raise
会重新抛出当前作用域中最后一个活动的异常。
所以,是的,这在Python 2中是一个深层次的问题,涉及到堆栈跟踪信息的存储——根据高地人传统,只有一个(保存到给定堆栈帧的堆栈跟踪对象)。因此,裸raise
重新抛出的异常是当前帧认为的“最后”异常,这不一定是我们人脑认为的在当前词法嵌套异常块中特定的那个异常。唉,作用域!
那么,在Python 3中修复了吗?
是的。怎么修复的?新的字节码指令(实际上有两个,还有一个隐式的在异常处理器开始时),但谁在乎呢——它“直观地”都能正常工作。你的示例代码不会再抛出RuntimeError: error from cleanup
,而是像预期的那样抛出RuntimeError: error from throws
。
我不能给你一个官方的理由,说明为什么这个没有包含在Python 2中。这个问题自PEP 344以来就被知道了,文中提到Raymond Hettinger在2003年提出了这个问题。如果我必须猜测,修复这个问题是一个破坏性更改(其中之一,它影响sys.exc_info
的语义),而这通常是一个不在小版本中做这个的好理由。
如果你在使用Python 2,有哪些选择:
1) 给你打算重新抛出的异常命名,并接受在堆栈跟踪底部多加一两行的事实。你的示例nested
函数变成:
def nested():
try:
throws()
except BaseException as e:
try:
cleanup()
except:
pass
raise e
以及相关的堆栈跟踪:
Traceback (most recent call last):
File "example", line 24, in main
nested()
File "example", line 17, in nested
raise e
RuntimeError: error from throws
所以,堆栈跟踪被修改了,但它仍然有效。
1.5) 使用raise
的三参数版本。很多人不知道这个,这是一个合法的(虽然有点笨重)保留堆栈跟踪的方法。
def nested():
try:
throws()
except:
e = sys.exc_info()
try:
cleanup()
except:
pass
raise e[0],e[1],e[2]
sys.exc_info
给我们一个包含(类型、值、堆栈跟踪)的三元组,这正是raise
的三参数版本所需要的。请注意,这个三参数语法仅在Python 2中有效。
2) 重构你的清理代码,使其不可能抛出未处理的异常。记住,这一切都与作用域有关——把try/except
移出nested
,放到它自己的函数中。
def nested():
try:
throws()
except:
cleanup()
raise
def cleanup():
try:
cleanup_code_that_totally_could_raise_an_exception()
except:
pass
def cleanup_code_that_totally_could_raise_an_exception():
raise RuntimeError('error from cleanup')
现在你就不用担心了;因为异常从未进入nested
的作用域,它不会干扰你打算重新抛出的异常。
3) 像之前那样使用裸raise
,然后接受这个事实;清理代码通常不会抛出异常,对吧?:-)