例外處理的幕後真相
電腦程式可能不會犯錯,但人類會,而且總是犯錯。有些是故意的,有些是無心的,有些則是無法預期的狀況。不管是哪種情況,我們都需要一個機制來處理這些問題,這就是例外處理(Exception Handling)的用途。
大部份程式語言都有類似的設計,在 Python 是使用 try...except...
關鍵字來處理例外,如果對於在 Python 怎麼使用 try...except...
有興趣,可以參考「為你自己學 Python」的錯誤處理章節介紹。這個章節我們要來看看例外處理的幕後真相,也就是在 CPython 裡面例外處理是怎麼實作的。
例外處理
我們先來個簡單的:
try:
1 / 0 # 這行會出錯
print("Hello World")
except Exception as e:
print(f"出事了阿伯! {e}")
執行上面這段程式,因為 1/0 會得到 ZeroDivisionError
例外,然後被 except
捕捉、處理。這是怎麼做到的?我們來看看這段程式碼編譯出來的 Bytecode 長什麼樣子,因為行數有點多,我就分段來說明:
2 4 LOAD_CONST 0 (1)
6 LOAD_CONST 1 (0)
8 BINARY_OP 11 (/)
12 POP_TOP
3 14 PUSH_NULL
16 LOAD_NAME 0 (print)
18 LOAD_CONST 2 ('Hello World')
20 CALL 1
28 POP_TOP
30 RETURN_CONST 4 (None)
>> 32 PUSH_EXC_INFO
// ... 略 ...
這段看起來滿簡單的,除了最後一行的 PUSH_EXC_INFO
之外沒有什麼新指令,不難看出就是在 try
區塊裡的那兩行程式碼。雖然我們知道在 BINARY_OP
指令做除法運算的時候應該要出錯,但那是執行時候的事,編譯 Bytecode 階段只是把這個指令編譯進去而已。除非是語法錯誤造成編譯失敗,不然編譯階段不會知道執行時候會不會或該不該出錯,直到 Python 的 VM 執行這段 Bytecode 才會知道。
堆堆堆堆疊
那麼 PUSH_EXC_INFO
指令在做什麼事?從名字看起來有點像是要把例外的資訊推到堆疊上,追一下原始碼:
inst(PUSH_EXC_INFO, (new_exc -- prev_exc, new_exc)) {
_PyErr_StackItem *exc_info = tstate->exc_info;
if (exc_info->exc_value != NULL) {
prev_exc = exc_info->exc_value;
}
else {
prev_exc = Py_None;
}
assert(PyExceptionInstance_Check(new_exc));
exc_info->exc_value = Py_NewRef(new_exc);
}
這個指令是把新的例外 new_exc
保存到當前執行緒的例外堆疊中 exc_info->exc_value
。如果本來就有異常還沒處理就先把它拿出來放到 prev_exc
以備將來使用。
為什麼會有例外堆疊這種東西?因為在例外處理不是百分百都能把問題解決,萬一在 except
區塊處理到一半也是可以繼續出包,然後丟給下一個例外處理區塊,萬一又出包就繼續這樣一路疊下去,所以有這個堆疊好像也是合理。這個 _PyErr_StackItem
是什麼?我們來看看這個結構:
typedef struct _err_stackitem {
PyObject *exc_value;
struct _err_stackitem *previous_item;
} _PyErr_StackItem;
這裡可以看到除了剛才看到的 exc_value
成員之外,還有另一個成員 previous_item
指向前一個例外堆疊,這樣就可以把例外堆疊串起來,讓我們可以在例外處理的時候一層一層的處理。
例外表
原本應該是繼續往下看,不過這裡有一些「變化」,我先拉到 Bytecode 最下面,看一個之前沒看過的 ExceptionTable
:
ExceptionTable:
4 to 28 -> 32 [0]
32 to 40 -> 84 [1] lasti
42 to 62 -> 74 [1] lasti
74 to 82 -> 84 [1] lasti
雖然沒看過,不過意思還算滿容易理解的,4 to 28 -> 32 [0]
表示從 Bytecode 的第 4 行到第 28 行之間的指令出錯的話,就跳到第 32 行指令。同理,其它三行也都是差不多的意思。而在 32 [0]
、74 [1]
以及 84[1]
後面看起來像是索引值的是指不同的例外處理,例如在同一個 try
可能會接好幾種 except
,這樣就可以知道是哪一個例外處理區塊要處理這個例外。