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

Tom*_*ell 10 python exception-handling

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

在他的文章中,Doug评论说处理是非直观的.中途期望它是Python版本中的一个bug或限制(大约在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())
Run Code Online (Sandbox Code Playgroud)

节目输出:

$ 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
Run Code Online (Sandbox Code Playgroud)

roi*_*ppi 17

盘旋回来回答.我首先回答你的问题.:-)

这真的有用吗?

def f():
    try:
        raise Exception('bananas!')
    except:
        pass
    raise
Run Code Online (Sandbox Code Playgroud)

那么,上面做了什么?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!
Run Code Online (Sandbox Code Playgroud)

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

def f():
    try:
        raise Exception('bananas!')
    except Exception as e:
        pass
    raise e
Run Code Online (Sandbox Code Playgroud)

现在怎么办?

# 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!
Run Code Online (Sandbox Code Playgroud)

异常语义在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`?
Run Code Online (Sandbox Code Playgroud)

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

所以,尴尬.例外是否应该特别限制在其封闭的词汇块中?Python 2说不,python 3说.但我在这里过分简化了一些事情; 裸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函数变为:

def nested():
    try:
        throws()
    except BaseException as e:
        try:
            cleanup()
        except:
            pass 
        raise e
Run Code Online (Sandbox Code Playgroud)

和相关的追溯:

Traceback (most recent call last):
  File "example", line 24, in main
    nested()
  File "example", line 17, in nested
    raise e
RuntimeError: error from throws
Run Code Online (Sandbox Code Playgroud)

因此,回溯会被改变,但它有效.

1.5)使用3参数版本raise.很多人都不知道这个,并且它是一种合法的(如果笨重的)保存堆栈跟踪的方法.

def nested():
    try:
        throws()
    except:
        e = sys.exc_info()
        try:
            cleanup()
        except:
            pass 
        raise e[0],e[1],e[2]
Run Code Online (Sandbox Code Playgroud)

sys.exc_info给我们一个包含(类型,值,回溯)的3元组,这正是3参数版本所raise采用的.请注意,这个3-arg语法仅适用于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')
Run Code Online (Sandbox Code Playgroud)

现在你不必担心; 由于异常从未使其成为nested范围,因此它不会干扰您打算重新引用的异常.

3)raise在你阅读所有这些内容之前使用裸露,就像你一样; 清理代码通常不会引发异常,对吧?:-)