绕回来回答。我首先不回答你的问题。 :-)
这真的有效吗?
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
and except
不是范围。事实上,Python 中几乎没有什么东西;我们有“LEGB 规则”来记住四个命名空间——本地、封闭、全局、内置。其他块根本不是范围;而是范围。我可以很高兴地宣布x
在一个for
循环并期望在该循环之后仍然能够引用它。
所以,尴尬。是否应该对异常进行特殊处理以将其限制在其封闭的词汇块中? Python 2 说不,Python 3 说yes http://www.python.org/dev/peps/pep-3110/#semantic-changes。但我在这里把事情过于简单化了;裸raise
是你最初问的问题,这些问题密切相关,但实际上并不相同。蟒蛇3could已强制规定命名异常的范围仅限于其块,而不涉及裸露的异常raise
thing.
裸露是什么意思raise
do‽
常见用法是使用裸raise
作为保留堆栈跟踪的一种方法。接住,记录/清理,再加注。很酷,我的清理代码没有出现在回溯中,99.9% 的时间都有效。但是,当我们尝试在异常处理程序中处理嵌套异常时,事情可能会出问题。有时。(请参阅底部的示例,了解何时存在/不存在问题)
凭直觉,无需争论raise
将正确处理嵌套异常处理程序,并找出要重新引发的正确“当前”异常。但这并不完全是现实。事实证明 - 在这里进入实现细节 - 异常信息被保存为当前的成员框架对象 https://docs.python.org/2/reference/datamodel.html#frame-objects。在 python 2 中,根本没有管道来处理单帧内堆栈上的推送/弹出异常处理程序;只是一个包含最后一个异常的字段,无论我们对它做了什么处理。就是这样裸露的raise
grabs.
6.9. 加薪声明 https://docs.python.org/2/reference/simple_stmts.html#the-raise-statement
raise_stmt ::= "raise" [expression ["," expression ["," expression]]]
如果不存在表达式,则 raise 重新引发最后一个异常
活跃于当前scope.
所以,是的,这是 python 2 中的一个深层问题,与如何存储回溯信息有关 - 在 Highlander 传统中,只能有一个(回溯对象保存到给定的堆栈帧)。结果,光秃秃的raise
重新引发当前框架认为是“最后”的异常,这不一定是我们人类大脑认为是我们当时所处的词法嵌套异常块所特有的异常。呸,scopes!
那么,在 python 3 中修复了吗?
是的。如何?新的字节码指令 https://docs.python.org/3.2/library/dis.html#opcode-POP_EXCEPT(实际上,除了处理程序之外,还有两个隐式的)但谁在乎呢——这一切都“直观地起作用”。而不是得到RuntimeError: error from cleanup
,你的示例代码引发RuntimeError: error from throws
正如预期的那样。
我无法给你一个官方原因,解释为什么它没有包含在 python 2 中。这个问题已经是已知的自 PEP 344 起 http://www.python.org/dev/peps/pep-0344/,其中提到 Raymond Hettinger 在2003。如果我必须guess,解决这个问题是一个重大变化(除其他外,它影响了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)使用 3 参数版本raise
。很多人不知道这个,它是保存堆栈跟踪的合法(如果笨重)方法。
def nested():
try:
throws()
except:
e = sys.exc_info()
try:
cleanup()
except:
pass
raise e[0],e[1],e[2]
sys.exc_info
给我们一个包含(类型、值、回溯)的三元组,这正是 3 参数版本的内容raise
需要。请注意,此 3-arg 语法仅适用于 python 2。
2)重构您的清理代码,使其无法possibly抛出未处理的异常。请记住,这一切都是关于scopes- 移动那个try/except
out of 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
就像您在阅读所有这些内容并接受它之前所做的那样;清理代码通常不会引发异常,对吧? :-)