找出异常上下文

时间:2013-10-07 20:28:35

标签: python exception python-2.7

tlndr:如何在函数中告诉它是否从except块(直接/间接)调用。 python2.7 / CPython的。

我使用python 2.7并尝试为我的自定义异常类提供与py3的__context__类似的东西:

class MyErr(Exception):
    def __init__(self, *args):
        Exception.__init__(self, *args)
        self.context = sys.exc_info()[1]
    def __str__(self):
        return repr(self.args) + ' from ' + repr(self.context)

这似乎工作正常:

try:
   1/0
except:
   raise MyErr('bang!')

#>__main__.MyErr: ('bang!',) from ZeroDivisionError('integer division or modulo by zero',)

有时我需要在异常块之外引发MyErr。这也很好:

raise MyErr('just so')

#>__main__.MyErr: ('just so',) from None

但是,如果在此之前存在处理的异常,则会将其错误地设置为MyErr的上下文:

try:
    print xxx
except Exception as e:
    pass

# ...1000 lines of code....
raise MyErr('look out')

#>__main__.MyErr: ('look out',) from NameError("name 'xxx' is not defined",) <-- BAD

我想原因是sys.exc_info只返回“last”而不是“current”异常:

  

此函数返回三个值的元组,这些值提供有关当前正在处理的异常的信息。 &LT; ...&GT;这里,“处理异常”被定义为“执行或已执行一个except子句。”

所以,我的问题是:如何判断解释器是否执行except子句(并且过去没有执行 )。换句话说:有没有办法知道MyErr.__init__堆栈中是否有except

我的应用程序不可移植,欢迎任何Cpython特定的黑客攻击。

4 个答案:

答案 0 :(得分:10)

使用CPython 2.7.3进行测试:

$ python myerr.py 
MyErr('bang!',) from ZeroDivisionError('integer division or modulo by zero',)
MyErr('nobang!',)

只要在except子句的范围内直接创建魔术异常,它就会起作用。但是,一些额外的代码可以解除这个限制。

import sys
import opcode

SETUP_EXCEPT = opcode.opmap["SETUP_EXCEPT"]
SETUP_FINALLY = opcode.opmap["SETUP_FINALLY"]
END_FINALLY = opcode.opmap["END_FINALLY"]

def try_blocks(co):
    """Generate code positions for try/except/end-of-block."""
    stack = []
    code = co.co_code
    n = len(code)
    i = 0
    while i < n:
        op = ord(code[i])
        if op in (SETUP_EXCEPT, SETUP_FINALLY):
            stack.append((i, i + ord(code[i+1]) + ord(code[i+2])*256))
        elif op == END_FINALLY:
            yield stack.pop() + (i,)
        i += 3 if op >= opcode.HAVE_ARGUMENT else 1

class MyErr(Exception):
    """Magic exception."""

    def __init__(self, *args):
        callee = sys._getframe(1)
        try:
            in_except = any(i[1] <= callee.f_lasti < i[2] for i in try_blocks(callee.f_code))
        finally:
            callee = None

        Exception.__init__(self, *args)
        self.cause = sys.exc_info()[1] if in_except else None

    def __str__(self):
        return "%r from %r" % (self, self.cause) if self.cause else repr(self)

if __name__ == "__main__":
    try:
        try:
            1/0
        except:
            x = MyErr('bang!')
            raise x
    except Exception as exc:
        print exc

    try:
        raise MyErr('nobang!')
    except Exception as exc:
        print exc
    finally:
        pass

请记住,“明确比隐含更好”,所以如果你问我这会更好:

try:
    …
except Exception as exc:
    raise MyErr("msg", cause=exc)

答案 1 :(得分:3)

以下方法可能会奏效,虽然它有点啰嗦。

  • import inspect; inspect.currentframe().f_code
  • 获取当前帧的代码
  • 检查字节码(f_code.co_code),可能使用dis.dis,以确定帧是否在except块中执行。
  • 根据你想要做的事情,你可能想要回到一个框架,看看是否没有从一个except块调用它。

例如:

def infoo():
    raise MyErr("from foo in except")

try:
    nope
except:
    infoo()
  • 如果except块中没有任何一个框架,则sys.exc_info()已过时。

答案 2 :(得分:1)

一个解决方案是在处理异常后调用sys.exc_clear()

import sys

class MyErr(Exception):
    def __init__(self, *args):
        Exception.__init__(self, *args)
        self.context = sys.exc_info()[1]
    def __str__(self):
        return repr(self.args) + ' from ' + repr(self.context)

try:
    print xxx
except Exception as e:
    # exception handled
    sys.exc_clear()

raise MyErr('look out')

给出:

Traceback (most recent call last):
  File "test.py", line 18, in <module>
    raise MyErr('look out')`
__main__.MyErr: ('look out',) from None

如果在没有提出MyErr的情况下处理异常的地方不多,则可能更适合修改对MyErr的调用,提供一些构造函数参数,甚至显式处理跟踪保留,如{{ 3}}

答案 3 :(得分:1)

我搜索了Python源代码,看看是否有一些指针在输入except块时被设置,可以通过从自定义异常的构造函数中查看帧序列来查询。

我发现这个fblocktype枚举存储在fblockinfo结构中:

enum fblocktype { LOOP, EXCEPT, FINALLY_TRY, FINALLY_END };

struct fblockinfo {
    enum fblocktype fb_type;
    basicblock *fb_block;
};

fblocktype上方有一条评论,描述框架块

  

帧块用于处理循环,try / except和try / finally。   它被称为一个框架块,以区别于它中的基本块   编译器IR。

然后当你向上看时有一个基本块的描述:

  

编译单元中的每个基本块都通过b_list链接   块被分配的逆序。 b_list指向下一个   阻止,不要与控制流接下来的b_next混淆。

此处还有关于Control Flow Graphs

的更多内容
  

控制流程图(通常由其首字母缩略词CFG引用)是a   有向图使用基本块对程序流进行建模   包含中间表示(缩写为“IR”,并在   这种情况是块内的Python字节码。基本块   它们本身就是一块具有单一入口点的IR   可能有多个退出点。单一入口点是关键   基本块;这一切都与跳跃有关。一个切入点是   改变控制流的东西的目标(例如函数调用   或者跳转)而退出点是可以改变的指令   程序流程(如跳转和'返回'语句)。这是什么   意思是基本块是从一开始的一大块代码   入口点并运行到出口点或块的末尾。

所有这些似乎表明Python设计中的框架块被视为临时对象。它不直接包含在控制流图中,除非作为包含基本块的字节代码的一部分,所以如果不解析帧字节代码,它似乎无法查询。

此外,我认为示例中sys.exc_info显示try块异常的原因是因为它存储了当前基本块的最后一个异常,这里不考虑框架块。

  

<强> sys.exc_info()

     

此函数返回三个提供信息值的元组   关于当前正在处理的异常。信息   返回特定于当前线程和当前线程   堆栈框架。如果当前堆栈帧没有处理异常,   信息来自调用堆栈帧或其调用者,   依此类推,直到找到处理异常的堆栈帧。   这里,“处理异常”被定义为“执行或拥有”   执行了一个except子句。“对于任何堆栈帧,只有信息   关于最近处理的异常是可以访问的。

所以,当它说堆栈帧时,我认为它具体意味着基本块,所有“处理异常”谈话意味着帧块中的异常,例如try/exceptfor等,冒充上面的基本块