找出异常上下文



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只是返回"最后一个"而不是"当前"异常:

此函数返回一个包含三个值的元组,这些值提供有关当前正在处理的异常的信息。 <...> 在这里,"处理异常"被定义为"执行或已经执行了 except 子句"。

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

我的应用程序不是便携式的,欢迎任何特定于Cpython的黑客。

这是在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)

以下方法可能有效,尽管它有点冗长。

  • 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()已过时。

一种解决方案是在处理异常后调用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 提供一些构造函数参数,甚至像本答案中那样显式处理回溯保留。

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

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

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 混淆,后者是下一个控制流。

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

控制流图(通常由其首字母缩略词 CFG 引用(是一个 使用基本块对程序流进行建模的有向图 包含中间表示形式(缩写为"IR",并在 这种情况是 Python 字节码(在块内。基本块 本身是一个具有单个入口点的 IR 块,但 可能有多个出口点。单一入口点是 基本块;这一切都与跳跃有关。入口点是 更改控制流的内容(如函数调用(的目标 或跳转(,而退出点是会改变 程序的流程(例如跳转和"返回"语句(。这是什么 意思是基本块是一段代码,从 入口点并运行到块的出口点或终点。

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

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

sys.exc_info((

此函数返回一个包含三个值的元组,这些值提供信息 关于当前正在处理的异常。信息介绍 返回的 return 既特定于当前线程,也特定于当前线程 堆栈帧。如果当前堆栈帧未处理异常, 信息取自调用堆栈帧或其调用方, 依此类推,直到找到正在处理异常的堆栈帧。 在这里,"处理异常"被定义为"执行或具有 执行了例外条款。对于任何堆栈帧,仅信息 关于最近处理的异常是可访问的。

所以当它说堆栈帧时,我认为它特别意味着基本块,所有"处理异常"的讨论都意味着帧块中的异常,如try/exceptfor等冒泡到上面的基本块

最新更新