以前的错误被当前的异常上下文屏蔽 [英] Previous error being masked by current exception context

查看:156
本文介绍了以前的错误被当前的异常上下文屏蔽的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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



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

 #!/ usr / bin / env python 

import sys
import traceback

def throws():
raise RuntimeError('throw from throws')

def nested():
try:
throws()
除了:
try:
cleanup()
except:
pass#忽略清理中的错误
raise#我们要重新提高原始错误

def cleanup():
raise RuntimeError('clean from cleanup')

def main():
try:
嵌套()
返回0
除了异常,错误:
traceback.print_exc()
return 1

如果__name__ =='__main__':
sys.exit(main())

程序输出:

  $ python masking_exceptions_catch。 py 
追溯(mos
文件masking_exceptions_catch.py​​,第24行,主要
nested()
文件masking_exceptions_catch.py​​,第14行,嵌套
清理()
文件masking_exceptions_catch.py​​,第20行,清理
raise RuntimeError('clean from cleanup')
RuntimeError:清除错误


解决方案

回到周围回答。我首先不回答你的问题。 : - )



这是否真的有效?

  def f():
try:
raise异常('bananas!')
除了:
pass
raise

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






好的,铅笔下来。

 #python 3.3 
4除了:
5 pass
----> 6 raise
7

RuntimeError:没有活动的异常来重新启动

#python 2.7
1 def f():
2 try:
----> 3提升异常('bananas!')
4除了
5 pass

异常:香蕉!

那是有成果的。为了好玩,我们来试试命名例外。

  def f():
try:
raise异常('香蕉!')
除了例外作为e:
pass
raise e

现在是什么?

 #python 3.3 
4除了例外为e:
5通过
----> 6 raise e
7

UnboundLocalError:赋值前引用的局部变量'e'

#python 2.7
4除了例外为e:
5 pass
----> 6加注e
7

例外:香蕉!

异常语义在python 2和3之间变化非常大。但是如果python 2的行为令人惊讶你在这里考虑:它基本上与python在其他地方一样。

  try:
1/0
除了异常作为e:
x = 4
#可以在异常块之后访问`x`? `e`怎么样?

尝试除了不是范围。实际上,在python中几乎没有什么;我们有LEGB规则来记住四个命名空间 - 本地,封闭,全局,内置。其他块根本不是范围;我可以在中为循环声明 x ,并期望仍然可以在该循环后引用它。 p>

所以,尴尬。特殊情况下是否应将异常限制在封闭的词汇块中? Python 2说不,python 3说。但我在这里过度简单化了;裸体 raise 是您最初询问的问题,并且问题密切相关,但实际上并不相同。 Python 3 可能要求将命名异常作用于其块,而不涉及裸露的 raise 事物。



裸体提高 do‽



常见的用法是使用裸机 raise 作为保留堆栈跟踪的一种手段。抓住,做日志/清理,重新编译。酷,我的清理代码没有出现在回溯中,工作99.9%的时间。但是,当我们尝试处理异常处理程序中的嵌套异常时,事情可能会向南。



直观地,无参数 raise 将正确处理嵌套的异常处理程序,并找出正确的当前异常来重新编译。这不是真正的现实。原来,在这里实现的细节 - 异常信息被保存为当前框架对象。而在python 2中,在单个框架内的堆栈中只需要处理push / popping异常处理程序即可。只是一个包含最后一个例外的字段,不管我们可能做了什么处理。这是裸露的提高抓取。


6.9。 加薪声明



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



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




所以,是的,这是python 2中的一个问题,与追溯信息的存储有关 - 在Highlander传统中,只能有一个(追溯对象保存到给定的堆栈帧)。因此,裸露的提高重新确定当前框架认为是最后一个异常,这不一定是我们人类大脑认为的特定于我们当时正在使用的词汇嵌套异常块。 Bah,范围!



所以,修复在python 3?



是的。怎么样? 新的字节码指令(其中两个实际上还有一个隐含的指令除了处理程序的开始),但真正关心 - 这一切都是直观的。而不是得到 RuntimeError:错误从清理,您的示例代码引发 RuntimeError:错误从throws 按预期。



我不能给你一个官方的原因,为什么这不包括在python 2.这个问题已经知道了自PEP 344 ,其中提到Raymond Hettinger在 2003 中提出了这个问题。如果我不得不猜测,那么修复这个是一个突破性的变化(除其他外,它会影响 sys.exc_info 的语义),这通常是一个很好的理由,不要在一个小的版本中。



选项,如果你在python 2:



1)命名要重新启动的异常,并处理一行或两行添加到堆栈跟踪的底部。您的示例嵌套函数变为:

  def nested():
try:
throws()
除了BaseException为e:
try:
cleanup()
除了:
pass
raise e

相关追踪:

追溯(最近的最后一次呼叫):
文件example,行24,在主
嵌套()
文件example,第17行,嵌套
raise e
RuntimeError:错误from throws

所以,追溯被改变,但它有效。



1.5)使用3参数版本的 raise 。很多人对此不了解,这是一种合法的(如果笨重的)保存堆栈跟踪的方法。

  def nested():
try:
throws()
除了:
e = sys.exc_info()
try:
cleanup()
除了:
pass
raise e [0],e [1],e [2]

sys.exc_info 给我们一个包含(类型,值,追溯)的3元组,这正是3参数版本的 raise take。请注意,这个3-arg语法仅适用于python 2。



2)重构您的清理代码,使其不可能抛出一个未处理的异常。记住,这一切都是关于范围 - 将尝试/除了嵌套自己的功能。

  def nested():
try:
throws()
除了
cleanup()
raise

def cleanup():
try:
cleanup_code_that_totally_could_raise_an_exception()
除了:


def cleanup_code_that_totally_could_raise_an_exception():
raise RuntimeError('clean from cleanup')

现在你不必担心;因为这个例外从未使它成为嵌套的范围,它不会干扰您打算重新提出的异常。



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


The following is an example I found at the website for Doug Hellman in a file named "masking_exceptions_catch.py". I can't locate the link at the moment. The exception raised in throws() is discarded while that raised by cleanup() is reported.

In his article, Doug remarks that the handling is non-intuitive. Halfway expecting it to be a bug or limitation in the Python version at the time it was written (circa 2009), I ran it in the current production release of Python for the Mac (2.7.6). It still reports the exception from cleanup(). I find this somewhat amazing and would like to see a description of how it is actually correct or desirable behavior.

#!/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())

Program output:

$ 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

解决方案

Circling back around to answer. I'll start by not answering your question. :-)

Does this really work?

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

So, what does the above do? Cue Jeopardy music.


Alright then, pencils down.

# 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!

Well, that was fruitful. For fun, let's try naming the exception.

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

What now?

# 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!

Exception semantics changed pretty drastically between python 2 and 3. But if python 2's behavior is at all surprising to you here, consider: it's basically in line with what python does everywhere else.

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

try and except are not scopes. Few things are, actually, in python; we have the "LEGB Rule" to remember the four namespaces - Local, Enclosing, Global, Builtin. Other blocks simply aren't scopes; I can happily declare x within a for loop and expect to still be able to reference it after that loop.

So, awkward. Should exceptions be special-cased to be confined to their enclosing lexical block? Python 2 says no, python 3 says yes. But I'm oversimplifying things here; bare raise is what you initially asked about, and the issues are closely related but not actually the same. Python 3 could have mandated that named exceptions are scoped to their block without addressing the bare raise thing.

What does bare raise do‽

Common usage is to use bare raise as a means to preserve the stack trace. Catch, do logging/cleanup, reraise. Cool, my cleanup code doesn't appear in the traceback, works 99.9% of the time. But things can go south when we try to handle nested exceptions within an exception handler. Sometimes. (see examples at the bottom for when it is/isn't a problem)

Intuitively, no-argument raise would properly handle nested exception handlers, and figure out the correct "current" exception to reraise. That's not exactly reality, though. Turns out that - getting into implementation details here - exception info is saved as a member of the current frame object. And in python 2, there's simply no plumbing to handle pushing/popping exception handlers on a stack within a single frame; there's just simply a field that contains the last exception, irrespective of any handling we may have done to it. That's what bare raise grabs.

6.9. The raise statement

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

If no expressions are present, raise re-raises the last exception that was active in the current scope.

So, yes, this is a problem deep within python 2 related to how traceback information is stored - in Highlander tradition, there can be only one (traceback object saved to a given stack frame). As a consequence, bare raise reraises what the current frame believes is the "last" exception, which isn't necessarily the one that our human brains believe is the one specific to the lexically-nested exception block we're in at the time. Bah, scopes!

So, fixed in python 3?

Yes. How? New bytecode instruction (two, actually, there's another implicit one at the start of except handlers) but really who cares - it all "just works" intuitively. Instead of getting RuntimeError: error from cleanup, your example code raises RuntimeError: error from throws as expected.

I can't give you an official reason why this was not included in python 2. The issue has been known since PEP 344, which mentions Raymond Hettinger raising the issue in 2003. If I had to guess, fixing this is a breaking change (among other things, it affects the semantics of sys.exc_info), and that's often a good enough reason not to do it in a minor release.

Options if you're on python 2:

1) Name the exception you intend to reraise, and just deal with a line or two being added to the bottom of your stack trace. Your example nested function becomes:

def nested():
    try:
        throws()
    except BaseException as e:
        try:
            cleanup()
        except:
            pass 
        raise e

And associated traceback:

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

So, the traceback is altered, but it works.

1.5) Use the 3-argument version of raise. A lot of people don't know about this one, and it is a legitimate (if clunky) way to preserve your stack trace.

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

sys.exc_info gives us a 3-tuple containing (type, value, traceback), which is exactly what the 3-argument version of raise takes. Note that this 3-arg syntax only works in python 2.

2) Refactor your cleanup code such that it cannot possibly throw an unhandled exception. Remember, it's all about scopes - move that try/except out of nested and into its own function.

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')

Now you don't have to worry; since the exception never made it to nested's scope, it won't interfere with the exception you intended to reraise.

3) Use bare raise like you were doing before you read all this and live with it; cleanup code doesn't usually raise exceptions, right? :-)

这篇关于以前的错误被当前的异常上下文屏蔽的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆