关于python:找出异常上下文

Finding out an exception context

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

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

1
2
3
4
5
6
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)

这似乎工作正常:

1
2
3
4
5
6
try:
   1/0
except:
   raise MyErr('bang!')

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

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

1
2
3
raise MyErr('just so')

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

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

1
2
3
4
5
6
7
8
9
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"异常:

This function returns a tuple of three values that give information about the exception that is currently being handled. <...> Here,"handling an exception" is defined as"executing or having executed an except clause."

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

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


这是使用CPython 2.7.3测试的:

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

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

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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

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

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


以下方法可能有效,尽管它有点啰嗦。

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

例如:

1
2
3
4
5
6
7
def infoo():
    raise MyErr("from foo in except")

try:
    nope
except:
    infoo()
  • 如果没有任何帧位于except块中,则sys.exc_info()已过时。


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

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

1
2
3
4
5
6
enum fblocktype { LOOP, EXCEPT, FINALLY_TRY, FINALLY_END };

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

fblocktype上方有一条评论描述了一个框架块:

A frame block is used to handle loops, try/except, and try/finally.
It's called a frame block to distinguish it from a basic block in the
compiler IR.

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

Each basicblock in a compilation unit is linked via b_list in the
reverse order that the block are allocated. b_list points to the next
block, not to be confused with b_next, which is next by control flow.

还在这里阅读有关控制流图的更多信息:

A control flow graph (often referenced by its acronym, CFG) is a
directed graph that models the flow of a program using basic blocks
that contain the intermediate representation (abbreviated"IR", and in
this case is Python bytecode) within the blocks. Basic blocks
themselves are a block of IR that has a single entry point but
possibly multiple exit points. The single entry point is the key to
basic blocks; it all has to do with jumps. An entry point is the
target of something that changes control flow (such as a function call
or a jump) while exit points are instructions that would change the
flow of the program (such as jumps and ‘return’ statements). What this
means is that a basic block is a chunk of code that starts at the
entry point and runs to an exit point or the end of the block.

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

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

sys.exc_info()

This function returns a tuple of three values that give information
about the exception that is currently being handled. The information
returned is specific both to the current thread and to the current
stack frame. If the current stack frame is not handling an exception,
the information is taken from the calling stack frame, or its caller,
and so on until a stack frame is found that is handling an exception.
Here,"handling an exception" is defined as"executing or having
executed an except clause." For any stack frame, only information
about the most recently handled exception is accessible.

因此,当它表示堆栈帧时,我认为它特别意味着基本块,并且所有"处理异常"谈话意味着帧块中的异常(例如try/exceptfor等)冒泡到基本块以上。


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

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

得到:

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

如果在没有提升MyErr的情况下处理异常的地方不多,则可能更适合修改对MyErr的调用,提供一些构造函数参数,或者甚至在此答案中显式处理回溯保留。