先前的错误被当前异常上下文屏蔽

时间:2014-05-17 04:21:08

标签: python exception-handling

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

在他的文章中,Doug评论说处理是非直观的。中途期望它是Python版本中的一个错误或限制(大约在2009年),我在Mac的当前生产版本中运行它(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 个答案:

答案 0 :(得分:17)

绕回来回答。我首先回答你的问题。 : - )

这真的有效吗?

def f():
    try:
        raise Exception('bananas!')
    except:
        pass
    raise

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


好吧,然后,铅笔下来。

# 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`?

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

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

raise 做什么

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

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

  

6.9。 The raise statement

     

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

     

如果没有表达式,则raise重新引发最后一个异常   在当前范围中处于活动状态。

所以,是的,这是python 2中与追踪信息存储方式有关的问题 - 在Highlander传统中,只能有一个(追踪对象保存到给定的堆栈帧)。因此,裸raise再次提出当前框架认为的最后一个"异常,这不一定是我们人类大脑所认为的那个异常是我们当时所处的词汇嵌套异常块所特有的异常。呸,范围!

那么,在python 3中修复了吗?

是。怎么样? New bytecode instruction(实际上,除了处理程序之外,还有另外一个隐含的处理程序),但真正关心的是 - 这一切都只是工作"直观。您的示例代码不是按RuntimeError: error from cleanup获得,而是按预期方式引发RuntimeError: error from throws

我无法正式解释为什么这不包含在python 2中。这个问题已为人所知since 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的3参数版本。很多人都不知道这个,并且这是一种合法的(如果笨重的)保存堆栈跟踪的方法。

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/exceptnested移出并进入自己的功能。

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,并使用它;清理代码通常不会引发异常,对吧? : - )