[英]How is exception handling implemented in Python?
這個問題要求解釋如何以各種語言在后台實現異常處理,但它沒有收到 Python 的任何響應。
我對 Python 特別感興趣,因為 Python 以某種方式“鼓勵”通過EAFP 原則拋出和捕獲異常。
我從其他 SO 答案中了解到,如果預計很少會引發異常,那么 try/catch 塊比 if/else 語句便宜,並且調用深度很重要,因為填充堆棧跟蹤很昂貴。 這可能對所有編程語言都適用。
python 的特別之處在於 EAFP 原則的高優先級。 python 異常是如何在參考實現(CPython)內部實現的?
try... except
在編譯器中有一些不錯的文檔:
/*
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, val, exc, exc] <evaluate E1> )
[tb, val, exc, exc, E1] JUMP_IF_NOT_EXC_MATCH L2 ) only if E1
[tb, val, exc] POP
[tb, val] <assign to V1> (or POP if no V1)
[tb] POP
[] <code for S1>
JUMP_FORWARD L0
[tb, val, exc] L2: DUP
.............................etc.......................
[tb, val, 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
。 這是在執行 RAISE_VARARGS 時在 ceval.c 中完成的。
那么在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
的。 正如我之前寫的,它是執行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, exc, 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, &exc, &val, &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
指令設置。 有了這個,我想我們已經繞了一圈! 哇!
異常文檔。
因此,這意味着異常本身也在 C 中實現(也在文檔中列出),這里有幾個例子:
PyExc_FileNotFoundError
PyExc_FileExistsError
PyExc_SyntaxError
ETC...
注意 -> 我不確定以下信息是否正確,但您可以在上述異常文檔中進行檢查。
我認為 CPython 檢查進程中的信號,如果信號被發送到給定進程 Python 檢查信號,這可能是一個例外。 不過我不確定。 不過,它確實會檢查信號!
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.