问题描述
This question 要求解释如何在各种语言的底层实现异常处理,但没有收到任何 Python 响应。
我对 Python 特别感兴趣,因为 Python 以某种方式“鼓励”通过 EAFP principle 抛出和捕获异常。
我从其他 SO 答案中了解到,如果预计很少引发异常,则 try/catch 块是 cheaper than an if/else statement,而 it's the call depth that's important 是因为填充堆栈跟踪的成本很高。这可能主要适用于所有编程语言。
python 的特别之处在于 EAFP 原则的高优先级。因此,python 异常如何在参考实现 (cpython) 中内部实现?
解决方法
try ... except
在 the compiler 中有一些不错的文档:
/*
Code generated for "try: S except E1 as V1: S1 except E2 as V2: S2 ...":
(The contents of the value stack is shown in [],with the top
at the right; 'tb' is trace-back info,'val' the exception's
associated value,and 'exc' the exception.)
Value stack Label Instruction Argument
[] SETUP_FINALLY L1
[] <code for S>
[] POP_BLOCK
[] JUMP_FORWARD L0
[tb,val,exc] L1: DUP )
[tb,exc,exc] <evaluate E1> )
[tb,E1] JUMP_IF_NOT_EXC_MATCH L2 ) only if E1
[tb,exc] POP
[tb,val] <assign to V1> (or POP if no V1)
[tb] POP
[] <code for S1>
JUMP_FORWARD L0
[tb,exc] L2: DUP
.............................etc.......................
[tb,exc] Ln+1: RERAISE # re-raise exception
[] L0: <next statement>
Of course,parts are not generated if Vi or Ei is not present.
*/
static int
compiler_try_except(struct compiler *c,stmt_ty s)
{
我们有:
- 一条
SETUP_FINALLY
指令,大概将L1
注册为发生异常时跳转到的位置(从技术上讲,我猜它会将其压入堆栈,因为必须恢复先前的值当我们的块完成时)。 -
S
的代码,即try:
块内的代码。 - 一条
POP_BLOCK
指令,用于清理内容(仅在 OK 情况下才能达到;我猜如果出现异常,VM 会自动执行) - 一个
JUMP_FORWARD
到 L0,它是下一条指令的位置(在try ... except
块之外)
这就是我们将在 OK 情况下运行的所有字节码。请注意,字节码不需要主动检查异常。相反,虚拟机会在出现异常时自动跳转到 L1
。这是在 ceval.c when executing RAISE_VARARGS 中完成的。
那么在 L1
会发生什么?简单地说,我们按顺序检查每个 except
子句:它是否与当前引发的异常匹配?如果是,我们运行该 except
块中的代码并跳转到 L0
(try ... except
块外的第一条指令)。如果不是,我们检查下一个 except
子句,或者如果没有子句匹配,则重新引发异常。
但让我们更具体一点。 dis
模块让我们转储字节码。所以让我们创建两个小的 python 文件。
一个检查:
tmp$ cat if.py
if type(x) is int:
x += 1
else:
print('uh-oh')
...还有一个捕获:
tmp$ cat try.py
try:
x += 1
except TypeError as e:
print('uh-oh')
现在,让我们转储他们的字节码:
tmp$ python3 -m dis if.py
1 0 LOAD_NAME 0 (type)
2 LOAD_NAME 1 (x)
4 CALL_FUNCTION 1
6 LOAD_NAME 2 (int)
8 COMPARE_OP 8 (is)
10 POP_JUMP_IF_FALSE 22
2 12 LOAD_NAME 1 (x)
14 LOAD_CONST 0 (1)
16 INPLACE_ADD
18 STORE_NAME 1 (x)
20 JUMP_FORWARD 8 (to 30)
4 >> 22 LOAD_NAME 3 (print)
24 LOAD_CONST 1 ('uh-oh')
26 CALL_FUNCTION 1
28 POP_TOP
>> 30 LOAD_CONST 2 (None)
32 RETURN_VALUE
对于成功的案例,这将运行 13 条指令(从 0 到 20,然后是 30 和 32)。
tmp$ python3 -m dis try.py
1 0 SETUP_EXCEPT 12 (to 14)
2 2 LOAD_NAME 0 (x)
4 LOAD_CONST 0 (1)
6 INPLACE_ADD
8 STORE_NAME 0 (x)
10 POP_BLOCK
12 JUMP_FORWARD 42 (to 56)
3 >> 14 DUP_TOP
16 LOAD_NAME 1 (TypeError)
18 COMPARE_OP 10 (exception match)
20 POP_JUMP_IF_FALSE 54
22 POP_TOP
24 STORE_NAME 2 (e)
26 POP_TOP
28 SETUP_FINALLY 14 (to 44)
4 30 LOAD_NAME 3 (print)
32 LOAD_CONST 1 ('uh-oh')
34 CALL_FUNCTION 1
36 POP_TOP
38 POP_BLOCK
40 POP_EXCEPT
42 LOAD_CONST 2 (None)
>> 44 LOAD_CONST 2 (None)
46 STORE_NAME 2 (e)
48 DELETE_NAME 2 (e)
50 END_FINALLY
52 JUMP_FORWARD 2 (to 56)
>> 54 END_FINALLY
>> 56 LOAD_CONST 2 (None)
58 RETURN_VALUE
对于成功的案例,这将运行 9 条指令(包括 0-12,然后是 56 和 58)。
现在,指令计数远不是所用时间的完美衡量标准(尤其是在字节码虚拟机中,指令的成本可能变化很大),但确实如此。
最后,让我们看看 CPython 如何“自动”跳转到 L1
。正如我之前写的,它作为 the execution of RAISE_VARARGS
的一部分发生:
case TARGET(RAISE_VARARGS): {
PyObject *cause = NULL,*exc = NULL;
switch (oparg) {
case 2:
cause = POP(); /* cause */
/* fall through */
case 1:
exc = POP(); /* exc */
/* fall through */
case 0:
if (do_raise(tstate,cause)) {
goto exception_unwind;
}
break;
default:
_PyErr_SetString(tstate,PyExc_SystemError,"bad RAISE_VARARGS oparg");
break;
}
goto error;
}
[...]
exception_unwind:
f->f_state = FRAME_UNWINDING;
/* Unwind stacks if an exception occurred */
while (f->f_iblock > 0) {
/* Pop the current block. */
PyTryBlock *b = &f->f_blockstack[--f->f_iblock];
if (b->b_type == EXCEPT_HANDLER) {
UNWIND_EXCEPT_HANDLER(b);
continue;
}
UNWIND_BLOCK(b);
if (b->b_type == SETUP_FINALLY) {
PyObject *exc,*val,*tb;
int handler = b->b_handler;
_PyErr_StackItem *exc_info = tstate->exc_info;
/* Beware,this invalidates all b->b_* fields */
PyFrame_BlockSetup(f,EXCEPT_HANDLER,f->f_lasti,STACK_LEVEL());
PUSH(exc_info->exc_traceback);
PUSH(exc_info->exc_value);
if (exc_info->exc_type != NULL) {
PUSH(exc_info->exc_type);
}
else {
Py_INCREF(Py_None);
PUSH(Py_None);
}
_PyErr_Fetch(tstate,&exc,&val,&tb);
/* Make the raw exception data
available to the handler,so a program can emulate the
Python main loop. */
_PyErr_NormalizeException(tstate,&tb);
if (tb != NULL)
PyException_SetTraceback(val,tb);
else
PyException_SetTraceback(val,Py_None);
Py_INCREF(exc);
exc_info->exc_type = exc;
Py_INCREF(val);
exc_info->exc_value = val;
exc_info->exc_traceback = tb;
if (tb == NULL)
tb = Py_None;
Py_INCREF(tb);
PUSH(tb);
PUSH(val);
PUSH(exc);
JUMPTO(handler);
if (_Py_TracingPossible(ceval2)) {
trace_info.instr_prev = INT_MAX;
}
/* Resume normal execution */
f->f_state = FRAME_EXECUTING;
goto main_loop;
}
} /* unwind stack */
有趣的部分是 JUMPTO(handler)
行。 handler
值来自 b->b_handler
,而 SETUP_FINALLY
又由 {{1}} 指令设置。有了这个,我想我们已经回到了原点!哇!
您可能知道 Python 有一个 C API,并且是用 C 编写的,因此它是用 C 语言 (-> CPython) 实现的。 CPython 使用一些函数来检查和处理异常,这里记录了它:
这意味着异常本身也在 C 中实现(也在文档中列出),这里有几个例子:
PyExc_FileNotFoundError
PyExc_FileExistsError
PyExc_SyntaxError
等等...
注意 -> 我不确定以下信息是否正确,但您可以在上述异常文档中进行事实检查。
我认为 CPython 会检查进程中的信号,如果信号被发送到给定进程 Python 会检查信号,这可能是一个例外。我不确定。不过,它确实会检查信号!