15

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

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

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子句(而不是过去执行过)。换句话说:有没有办法知道堆栈上MyErr.__init__是否有一个向上的?except

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

4

4 回答 4

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)
于 2013-10-10T02:04:22.810 回答
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()已过时。
于 2013-10-10T05:08:24.770 回答
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提供一些构造函数参数更合适,或者甚至像this answer中那样显式处理回溯保存。

于 2013-10-09T21:47:32.887 回答
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 混淆,b_next 是控制流的下一个。

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

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

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

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

sys.exc_info()

此函数返回三个值的元组,这些值提供有关当前正在处理的异常的信息。返回的信息特定于当前线程和当前堆栈帧。如果当前堆栈帧未处理异常,则从调用堆栈帧或其调用者获取信息,依此类推,直到找到正在处理异常的堆栈帧。在这里,“处理异常”被定义为“执行或已经执行了一个例外子句”。对于任何堆栈帧,只能访问有关最近处理的异常的信息。

因此,当它在那里说堆栈帧时,我认为它特别表示基本块,并且所有“处理异常”的讨论都意味着帧块中的异常,例如 a try/exceptfor等等。冒泡到上面的基本块

于 2013-10-14T18:10:42.983 回答