关于python:当前异常上下文屏蔽了以前的错误

Previous error being masked by current exception context

以下是我在Doug Hellman网站上发现的一个名为"masking_exceptions_catch.py"的文件中的示例。 我暂时无法找到该链接。 抛出throws()中引发的异常,同时报告由cleanup()引发的异常。

在他的文章中,Doug评论说处理是非直观的。 中途期望它是Python版本中的一个bug或限制(大约在2009年),我在Mac的当前生产版本中运行它(2.7.6)。 它仍然报告cleanup()的异常。 我发现这有点惊人,并希望看到它是如何实际正确或理想的行为的描述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!/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())

节目输出:

1
2
3
4
5
6
7
8
9
$ 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
2
3
4
5
6
def f():
    try:
        raise Exception('bananas!')
    except:
        pass
    raise

那么,上面做了什么? Cue Jeopardy音乐。

好吧,然后,铅笔下来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 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!

嗯,那是富有成效的。为了好玩,让我们尝试命名异常。

1
2
3
4
5
6
def f():
    try:
        raise Exception('bananas!')
    except Exception as e:
        pass
    raise e

现在怎么办?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 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一致。

1
2
3
4
5
try:
    1/0
except Exception as e:
    x=4
#can I access `x` here after the exception block?  How about `e`?

tryexcept不是范围。实际上,很少有东西在python中;我们有"LEGB规则"来记住四个命名空间 - Local,Enclosing,Global,Builtin。其他块根本不是范围;我可以愉快地在for循环中声明x并期望在该循环之后仍能够引用它。

所以,尴尬。例外是否应该特别限制在其封闭的词汇块中? Python 2说不,python 3说是。但我在这里过分简化了事情; bare raise是您最初询问的内容,问题密切相关但实际上并不相同。 Python 3可能已经强制要求将命名异常限定在其块中,而不解决裸raise事物。

raise做什么?

常见用法是使用bare raise作为保留堆栈跟踪的方法。捕获,进行记录/清理,再加注。很酷,我的清理代码没有出现在追溯中,99.9%的时间都有效。但是当我们尝试在异常处理程序中处理嵌套异常时,事情就会向南发展。有时。 (参见底部的示例,了解它何时/不是问题)

直观地说,无参数raise将正确处理嵌套的异常处理程序,并找出正确的"当前"异常来重新加载。但这并不完全是现实。事实证明 - 在此处了解实现细节 - 异常信息被保存为当前帧对象的成员。在python 2中,根本没有管道来处理在单个帧内的堆栈上推送/弹出异常处理程序;只是一个包含最后一个异常的字段,无论我们对它做了什么处理。这就是raise抓住的东西。

6.9。提高声明

raise_stmt ::= "raise" [expression ["," expression ["," expression]]]

如果没有表达式,则raise重新引发最后一个异常
在当前范围内活跃。

所以,是的,这是python 2深层次的问题,与回溯信息的存储方式有关 - 在Highlander传统中,只能有一个(回溯对象保存到给定的堆栈帧)。因此,裸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函数变为:

1
2
3
4
5
6
7
8
9
def nested():
    try:
        throws()
    except BaseException as e:
        try:
            cleanup()
        except:
            pass
        raise e

和相关的追溯:

1
2
3
4
5
6
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的3参数版本。很多人都不知道这个,并且它是一种合法的(如果笨重的)保存堆栈跟踪的方法。

1
2
3
4
5
6
7
8
9
10
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参数版本所采用的。请注意,这个3-arg语法仅适用于python 2。

2)重构清理代码,使其无法抛出未处理的异常。请记住,这都是关于范围的 - 将try/except移出nested并移动到自己的函数中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;清理代码通常不会引发异常,对吧? :-)

好。