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/except
、for
等冒泡到上面的基本块。