虛擬機(jī)是如何捕獲異常的?
楔子
既然虛擬機(jī)內(nèi)建的異常處理動作我們已經(jīng)了解了,那么接下來就看看異常捕獲是如何實(shí)現(xiàn)的,還有它又是如何影響虛擬機(jī)的異常處理流程的。畢竟在大部分情況下,我們都不會將異常拋出去,而是將它捕獲起來。
異常捕獲語句
這里先來回顧一下異常捕獲語句,首先一個(gè)完整的異常捕獲語句如下。
try:
pass
except IndexError as e:
pass
except Exception as e:
pass
else:
pass
finally:
pass
情況可以分為以下幾種:
1)如果 try 里面的代碼在執(zhí)行時(shí)沒有出現(xiàn)異常,那么會執(zhí)行 else ,然后執(zhí)行 finally。
try:
print("我是 try")
except Exception as e:
print("我是 except")
else:
print("我是 else")
finally:
print("我是 finally")
"""
我是 try
我是 else
我是 finally
"""
2)如果 try 里面的代碼在執(zhí)行時(shí)出現(xiàn)異常了(異常會被設(shè)置在線程狀態(tài)對象中),那么會依次判斷 except(可以有多個(gè))能否匹配發(fā)生的異常。如果某個(gè) except 將異常捕獲了,那么會將異常給清空,然后執(zhí)行 finally。
try:
raise IndexError("IndexError Occurred")
except ValueError as e:
print("ValueError 匹配上了異常")
except IndexError as e:
print("IndexError 匹配上了異常")
except Exception as e:
print("Exception 匹配上了異常")
else:
print("我是 else")
finally:
print("我是 finally")
"""
IndexError 匹配上了異常
我是 finally
"""
except 子句可以有很多個(gè),發(fā)生異常時(shí)會從上往下依次匹配。但是注意:多個(gè) except 子句最多只有一個(gè)被執(zhí)行,比如當(dāng)前的 IndexError 和 Exception 都能匹配發(fā)生的異常,但只會執(zhí)行匹配上的第一個(gè) except 子句。
另外只要發(fā)生異常了,else 就不會執(zhí)行了。不管 except 有沒有將異常捕獲到,都不會執(zhí)行 else,因?yàn)?else 只有在 try 里面沒有發(fā)生異常的時(shí)候才會執(zhí)行。
3)如果 try 里面的代碼在執(zhí)行時(shí)出現(xiàn)異常了,但 except 沒有將異常捕獲掉,那么異常仍然被保存在線程狀態(tài)對象中,然后執(zhí)行 finally。如果 finally 子句中沒有出現(xiàn) return、break、continue 等關(guān)鍵字,再將異常拋出來。
try:
raise IndexError("IndexError Occurred")
except ValueError:
print("ValueError 匹配上了異常")
finally:
print("我是 finally")
"""
我是 finally
Traceback (most recent call last):
File "......", line 2, in <module>
raise IndexError("IndexError Occurred")
IndexError: IndexError Occurred
"""
except 沒有將異常捕獲掉,所以執(zhí)行完 finally 之后,異常又被拋出來了。但如果 finally 里面出現(xiàn)了 return、break、continue 等關(guān)鍵字,也不會拋出異常,而是將異常丟棄掉。
def f():
try:
raise IndexError("IndexError Occurred")
except ValueError:
print("ValueError 匹配上了異常")
finally:
print("我是 finally")
return
f()
"""
我是 finally
"""
def g():
for i in range(3):
try:
raise IndexError("IndexError Occurred")
except ValueError:
print("ValueError 匹配上了異常")
finally:
print(f"我是 finally,i = {i}")
continue
g()
"""
我是 finally,i = 0
我是 finally,i = 1
我是 finally,i = 2
"""
由于 finally 里面出現(xiàn)了 return 和 continue,所以異常并沒有發(fā)生,而是被丟棄掉了。這個(gè)特性相信有很多小伙伴之前還是沒有發(fā)現(xiàn)的。
然后 try、except、else、finally 這幾個(gè)關(guān)鍵字不需要同時(shí)出現(xiàn),可以有以下幾種組合。
try ... except
try ... finally
try ... except ... else
try ... except ... else ... finally
注意里面的 except,可以出現(xiàn)多次,但其它關(guān)鍵字在一個(gè) try 語句內(nèi)只能出現(xiàn)一次。
返回值問題
如果這幾個(gè)關(guān)鍵字對應(yīng)的代碼塊都指定了返回值,那么聽誰的呢?下面解釋一下。
def retval():
try:
return 123
except Exception:
return 456
print(retval()) # 123
由于沒有發(fā)生異常,所以返回了 try 指定的返回值。
def retval():
try:
return 123
except Exception:
return 456
else:
return 789
print(retval()) # 123
雖然指定了 else,但是 try 里面已經(jīng)執(zhí)行 return 了,所以打印的仍是 try 的返回值。
def retval():
try:
1 / 0
return 123
except Exception:
return 456
print(retval()) # 456
由于發(fā)生異常,所以返回了 except 指定的返回值。
def retval():
try:
1 / 0
return 123
except Exception:
return 456
else:
return 789
print(retval()) # 456
一旦發(fā)生異常,else 就不可能執(zhí)行,所以此時(shí)仍然返回 456。
def retval():
try:
return 123
except Exception:
return 456
finally:
pass
print(retval()) # 123
finally 永遠(yuǎn)會執(zhí)行,但它沒有指定返回值,所以此時(shí)返回的是 123。
def retval():
try:
return 123
except Exception:
return 456
finally:
return
print(retval()) # None
一旦 finally 中出現(xiàn)了 return,那么返回的都是 finally 指定的返回值。并且此時(shí)即便出現(xiàn)了沒有捕獲的異常,也不會報(bào)錯,因?yàn)闀惓G棄掉。
def retval():
try:
return 123
except Exception:
return 456
finally:
pass
return 789
print(retval()) # 123
函數(shù)一旦 return,就表示要返回了,但如果這個(gè) return 是位于出現(xiàn)了 finally 的異常捕獲語句中,那么會先執(zhí)行 finally,然后再返回。所以最后的 return 789 是不會執(zhí)行的,因?yàn)橐呀?jīng)出現(xiàn) return 了,finally 執(zhí)行完畢之后就直接返回了。
def retval():
try:
pass
except Exception:
return 456
finally:
pass
return 789
print(retval()) # 789
沒有異常,所以 except 里的 return 不會執(zhí)行,而 try 和 finally 里面也沒有 return,因此返回 789。
一個(gè)簡單的異常捕獲,總結(jié)起來還稍微有點(diǎn)繞呢。
從 Python 的層面理解完異常捕獲之后,再來看看虛擬機(jī)是如何實(shí)現(xiàn)這一機(jī)制的?想要搞清楚這一點(diǎn),還是得從字節(jié)碼入手。
異常捕獲對應(yīng)的字節(jié)碼
隨便寫一段代碼,然后反編譯一下。
import dis
code_string = """
try:
raise Exception("拋出一個(gè)異常")
except Exception as e:
print(e)
finally:
print("我一定會被執(zhí)行的")
"""
dis.dis(compile(code_string, "exception", "exec"))
拋異常有兩種方式,一種是虛擬機(jī)執(zhí)行的時(shí)候出現(xiàn)錯誤而拋出異常,另一種是使用 raise 關(guān)鍵字手動拋出異常。這里我們就用第二種方式,來看一下反編譯的結(jié)果(為了清晰,省略掉了源代碼行號)。
0 RESUME 0
2 NOP
4 PUSH_NULL
6 LOAD_NAME 0 (Exception)
8 LOAD_CONST 0 ('拋出一個(gè)異常')
10 CALL 1
18 RAISE_VARARGS 1
>> 20 PUSH_EXC_INFO
22 LOAD_NAME 0 (Exception)
24 CHECK_EXC_MATCH
26 POP_JUMP_IF_FALSE 18 (to 64)
28 STORE_NAME 1 (e)
30 PUSH_NULL
32 LOAD_NAME 2 (print)
34 LOAD_NAME 1 (e)
36 CALL 1
44 POP_TOP
46 POP_EXCEPT
48 LOAD_CONST 1 (None)
50 STORE_NAME 1 (e)
52 DELETE_NAME 1 (e)
54 JUMP_FORWARD 8 (to 72)
>> 56 LOAD_CONST 1 (None)
58 STORE_NAME 1 (e)
60 DELETE_NAME 1 (e)
62 RERAISE 1
>> 64 RERAISE 0
>> 66 COPY 3
68 POP_EXCEPT
70 RERAISE 1
>> 72 NOP
74 PUSH_NULL
76 LOAD_NAME 2 (print)
78 LOAD_CONST 2 ('我一定會被執(zhí)行的')
80 CALL 1
88 POP_TOP
90 RETURN_CONST 1 (None)
>> 92 PUSH_EXC_INFO
94 PUSH_NULL
96 LOAD_NAME 2 (print)
98 LOAD_CONST 2 ('我一定會被執(zhí)行的')
100 CALL 1
108 POP_TOP
110 RERAISE 0
>> 112 COPY 3
114 POP_EXCEPT
116 RERAISE 1
ExceptionTable:
4 to 18 -> 20 [0]
20 to 28 -> 66 [1] lasti
30 to 44 -> 56 [1] lasti
46 to 54 -> 92 [0]
56 to 64 -> 66 [1] lasti
66 to 70 -> 92 [0]
92 to 110 -> 112 [1] lasti
字節(jié)碼指令還是比較多的,我們來分段解釋。
try 子句的指令
try 子句的指令如下。
6 LOAD_NAME 0 (Exception)
8 LOAD_CONST 0 ('拋出一個(gè)異常')
10 CALL 1
18 RAISE_VARARGS 1
6 LOAD_NAME 指令會將 <class 'Exception'> 壓入運(yùn)行時(shí)棧。8 LOAD_CONST 指令會將字符串常量壓入運(yùn)行時(shí)棧。然后 10 CALL 指令將運(yùn)行時(shí)棧里的元素彈出,進(jìn)行調(diào)用??梢钥吹讲还苁钦{(diào)用函數(shù),還是調(diào)用類,執(zhí)行的都是 CALL 指令,然后將返回值(這里就是 Exception 對象,即異常)壓入棧中。
接著執(zhí)行 18 RAISE_VARARGS,這是一條新指令,看一下它的邏輯。
TARGET(RAISE_VARARGS) {
PyObject **args = (stack_pointer - oparg);
#line 606 "Python/bytecodes.c"
PyObject *cause = NULL, *exc = NULL;
switch (oparg) {
case 2:
cause = args[1];
/* fall through */
case 1:
exc = args[0];
/* fall through */
case 0:
// 調(diào)用 do_raise 設(shè)置異常
if (do_raise(tstate, exc, cause)) {
assert(oparg == 0);
monitor_reraise(tstate, frame, next_instr-1);
goto exception_unwind;
}
break;
default:
_PyErr_SetString(tstate, PyExc_SystemError,
"bad RAISE_VARARGS oparg");
break;
}
if (true) { STACK_SHRINK(oparg); goto error; }
#line 912 "Python/generated_cases.c.h"
}
因?yàn)?nbsp;RAISE_VARARGS 指令的參數(shù)是 1,所以 case 1 成立,于是將異常從運(yùn)行時(shí)棧中彈出,并賦值給變量 exc,然后調(diào)用 do_raise 函數(shù)。
在 do_raise 中,最終會調(diào)用之前說過的 PyErr_Restore 函數(shù),將異常對象存儲到當(dāng)前的線程狀態(tài)對象中,然后跳轉(zhuǎn)到標(biāo)簽為 exception_unwind 的地方開始異常捕獲。
exception_unwind:
{
// INSTR_OFFSET 是一個(gè)宏,定義在 Python/ceval_macros.h 中
// #define INSTR_OFFSET() ((int)(next_instr - _PyCode_CODE(frame->f_code)))
int offset = INSTR_OFFSET()-1;
int level, handler, lasti;
// 查詢 co_exceptiontable,即異常處理表
// 當(dāng) try 里面產(chǎn)生異常時(shí),那么必須跳轉(zhuǎn)到相應(yīng)的 except 或 finally 里面
// 在 Python 3.10 以及之前的版本,這個(gè)機(jī)制是通過引入一個(gè)獨(dú)立的動態(tài)棧,跟蹤 try 語句塊實(shí)現(xiàn)的
// 但從 3.11 開始,動態(tài)棧被替換成了靜態(tài)表,即異常處理表,由 co_exceptiontable 字段維護(hù)
// 并且表在編譯期間就靜態(tài)生成了,是一段字節(jié)序列,里面包含了 try / except / finally 信息
// 當(dāng)代碼在執(zhí)行過程中出現(xiàn)異常時(shí),解釋器會查詢這張表,尋找與之匹配的 except / finall 塊
if (get_exception_handler(frame->f_code, offset, &level, &handler, &lasti) == 0)
{
// No handlers, so exit.
// ...
// 如果 get_exception_handler 返回值等于 0,說明沒有找到
// 那么跳轉(zhuǎn)到 exit_unwind 標(biāo)簽,退出當(dāng)前棧幀
goto exit_unwind;
}
// 否則說明找到了,那么要進(jìn)行跳轉(zhuǎn),而跳轉(zhuǎn)地址保存在 handler 中
assert(STACK_LEVEL() >= level);
// ...
// 獲取 tstate->current_exception,即當(dāng)前線程狀態(tài)對象保存的異常
PyObject *exc = _PyErr_GetRaisedException(tstate);
// 壓入棧中
PUSH(exc);
// 跳轉(zhuǎn)到指定指令,即 except / finally 塊對應(yīng)的指令
JUMPTO(handler);
if (monitor_handled(tstate, frame, next_instr, exc) < 0) {
goto exception_unwind;
}
/* Resume normal execution */
DISPATCH();
}
該指令執(zhí)行后,異常會被壓入棧中,虛擬機(jī)也知道該跳轉(zhuǎn)到什么地方了。
except 子句的指令
try 子句的指令我們說完了,再來看看 except 子句。
>> 20 PUSH_EXC_INFO
22 LOAD_NAME 0 (Exception)
24 CHECK_EXC_MATCH
26 POP_JUMP_IF_FALSE 18 (to 64)
28 STORE_NAME 1 (e)
30 PUSH_NULL
32 LOAD_NAME 2 (print)
34 LOAD_NAME 1 (e)
36 CALL 1
44 POP_TOP
46 POP_EXCEPT
48 LOAD_CONST 1 (None)
50 STORE_NAME 1 (e)
52 DELETE_NAME 1 (e)
54 JUMP_FORWARD 8 (to 72)
首先執(zhí)行 20 PUSH_EXC_INFO 指令,內(nèi)部邏輯如下。
TARGET(PUSH_EXC_INFO) {
// RAISE_VARARGS 指令將異常設(shè)置在了線程狀態(tài)對象中
// 然后跳轉(zhuǎn)到 exception_unwind 標(biāo)簽,將異常壓入運(yùn)行時(shí)棧
PyObject *new_exc = stack_pointer[-1];
PyObject *prev_exc;
#line 2543 "Python/bytecodes.c"
/* tstate->current_exception 表示當(dāng)前存儲的異常
* tstate->exc_info 是一個(gè)結(jié)構(gòu)體,相當(dāng)于對異常做了一個(gè)封裝
*
* typedef struct _err_stackitem {
* PyObject *exc_value;
* struct _err_stackitem *previous_item;
* } _PyErr_StackItem;
*/
_PyErr_StackItem *exc_info = tstate->exc_info;
// exc_info->exc_value 還是之前存儲的異常
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 替換為新產(chǎn)生的異常
exc_info->exc_value = Py_NewRef(new_exc);
#line 3584 "Python/generated_cases.c.h"
// 此時(shí)棧里面有兩個(gè)元素,從棧底到棧頂分別是舊異常、新異常
STACK_GROW(1);
stack_pointer[-1] = new_exc;
stack_pointer[-2] = prev_exc;
DISPATCH();
}
該指令做的事情是將舊異常和新異常壓入運(yùn)行時(shí)棧。
22 LOAD_NAME 加載 Exception,然后執(zhí)行 24 CHECK_EXC_MATCH,邏輯如下。
TARGET(CHECK_EXC_MATCH) {
// 獲取棧頂元素,由于源碼中是 except Exception as e
// 所以會得到上一條 LOAD_NAME 指令壓入的 class Exception
PyObject *right = stack_pointer[-1];
// 執(zhí)行 exception_unwind 標(biāo)簽內(nèi)的邏輯時(shí)壓入的異常
PyObject *left = stack_pointer[-2];
PyObject *b;
#line 2122 "Python/bytecodes.c"
// ...
// 判斷異常對象和指定的異常類能否匹配
int res = PyErr_GivenExceptionMatches(left, right);
#line 2981 "Python/generated_cases.c.h"
Py_DECREF(right);
#line 2130 "Python/bytecodes.c"
b = res ? Py_True : Py_False;
#line 2985 "Python/generated_cases.c.h"
// 將棧頂元素用 res 替換掉,此時(shí)棧里面有兩個(gè)元素
// 從棧底到棧頂分別是:舊異常對象,新異常對象,布爾值(異常是否可以被捕獲)
stack_pointer[-1] = b;
DISPATCH();
}
26 POP_JUMP_IF_FALSE 會彈出棧頂元素,如果為 False,說明異常無法被捕獲,那么要跳轉(zhuǎn)到下一個(gè) except 或者 finally。如果可以被捕獲,那么執(zhí)行 28 STORE_NAME,再將棧里的異常對象彈出,賦值給變量 e。
到此 except Exception as e 這一行語句便已經(jīng)完成,至于接下來的 4 條指令應(yīng)該不需要解釋了。
圖片
很好理解,這 4 條就是 print(e) 對應(yīng)的指令,然后執(zhí)行 46 POP_EXCEPT,邏輯如下。
TARGET(POP_EXCEPT) {
// 此時(shí)運(yùn)行時(shí)棧里面還剩下一個(gè)舊異常
PyObject *exc_value = stack_pointer[-1];
#line 930 "Python/bytecodes.c"
_PyErr_StackItem *exc_info = tstate->exc_info;
// 更新引用計(jì)數(shù)
Py_XSETREF(exc_info->exc_value, exc_value);
#line 1274 "Python/generated_cases.c.h"
STACK_SHRINK(1);
DISPATCH();
}
但是接下來的幾條指令是干嘛的,估計(jì)有人會感到困惑。
圖片
這幾條指令的具體作用,我們稍后解釋。
異常跳轉(zhuǎn)表
finally 子句對應(yīng)的指令比較簡單,我們就不看了。相比之前版本,3.12 的異常捕獲變得簡單許多,因?yàn)橄嚓P(guān)信息都靜態(tài)化了。在以前的版本中是使用 SETUP_FINALLY 等指令來處理異常,而 3.12 換成了更高效的異常表結(jié)構(gòu),類似于 Java 的異常表。
我們來看一下異常表的結(jié)構(gòu),它由 PyCodeObject 對象的 co_exceptiontable 字段負(fù)責(zé)維護(hù)。
圖片
4 to 18 -> 20
表示 try 子句內(nèi)部對應(yīng)偏移量為 4 ~ 18 的指令,并且如果出現(xiàn)異常,那么跳轉(zhuǎn)到偏移量為 20 的指令。
20 to 28 -> 66
偏移量為 20 ~ 28 的指令對應(yīng) except 子句本身,如果執(zhí)行出錯,跳轉(zhuǎn)到偏移量為 66 的指令去清理異常。
30 to 44 -> 56
偏移量為 30 ~ 44 的指令對應(yīng) except 子句內(nèi)部的處理邏輯,如果執(zhí)行出錯則跳轉(zhuǎn)到第 56 條指令。
圖片
注意里面的 DELETE_NAME,它是 del 語句對應(yīng)的指令。所以跳轉(zhuǎn)之后的這幾條指令,負(fù)責(zé)刪除變量 e,怎么理解呢?我們舉個(gè)例子。
e = 2.71
try:
raise Exception("xx")
except Exception as e:
pass
print(e)
"""
NameError: name 'e' is not defined
"""
奇怪,為什么在外面打印變量 e 報(bào)錯了呢?其實(shí) Python 會對 except 子句內(nèi)部做一些處理,以上代碼最終會變成下面這個(gè)樣子。
e = 2.71
try:
raise Exception("xx")
except Exception as e:
e = None
try:
pass
finally:
del e
finally:
print(e)
至于這么做的原因,稍后解釋。
46 to 54 -> 92
del e 相關(guān)指令,但它對應(yīng)的是存在 finally 的情況,刪除之后會跳轉(zhuǎn)到偏移量為 92 的指令。
56 to 64 -> 66
del e 相關(guān)指令,對應(yīng)不存在 finally 的情況。
66 to 70 -> 92
異常清理相關(guān)指令。
92 to 110 -> 112
finally 子句對應(yīng)的指令。
我們看到 try / except / finally 塊的范圍信息、異常處理的起始位置、需要執(zhí)行的清理操作都被靜態(tài)化了,在編譯階段就已經(jīng)確定,所以性能方面比之前要更高。并且當(dāng) try 子句內(nèi)的代碼沒有出現(xiàn)錯誤時(shí),和不使用異常捕獲之間基本沒有性能差異。
總之 Python 中一旦出現(xiàn)異常了,那么會將異常類型、異常值、異?;厮輻TO(shè)置在線程狀態(tài)對象中,然后棧幀一步一步地回退,尋找異常捕獲代碼(從內(nèi)向外)。如果退到了模塊級別還沒有發(fā)現(xiàn)異常捕獲,那么從外向內(nèi)打印 traceback 中的信息,當(dāng)走到最內(nèi)層的時(shí)候再將線程中設(shè)置的異常類型和異常值打印出來。
def h():
1 / 0
def g():
h()
def f():
g()
f()
# traceback 回溯棧
Traceback (most recent call last):
# 打印模塊的 traceback
# 并提示:發(fā)生錯誤是因?yàn)樵诘?10 行調(diào)用了 f()
File "/Users/.../main.py", line 10, in <module>
f()
# 打印函數(shù) f 的 traceback
# 并提示:發(fā)生錯誤是因?yàn)樵诘?8 行調(diào)用了 g()
File "/Users/.../main.py", line 8, in f
g()
# 打印函數(shù) g 的 traceback
# 并提示:發(fā)生錯誤是因?yàn)樵诘?5 行調(diào)用了 h()
File "/Users/.../main.py", line 5, in g
h()
# 打印函數(shù) h 的 traceback
# 并提示:發(fā)生錯誤是因?yàn)樵诘?2 行執(zhí)行了 1 / 0
File "/Users/.../main.py", line 2, in h
1 / 0
# 函數(shù) h 的 traceback -> tb_next 為 None,證明錯誤是發(fā)生在函數(shù) h 中
# 在模塊中調(diào)用函數(shù) f 相當(dāng)于導(dǎo)火索,然后一層一層輸出,最終定位到函數(shù) h
# 然后再將之前設(shè)置在線程狀態(tài)對象中的異常類型和異常值打印出來即可
ZeroDivisionError: division by zero
模塊中調(diào)用了函數(shù) f,函數(shù) f 調(diào)用了函數(shù) g,函數(shù) g 調(diào)用了函數(shù) h。然后在函數(shù) h 中執(zhí)行出錯了,但又沒有異常捕獲,那么會將執(zhí)行權(quán)交給函數(shù) g 對應(yīng)的棧幀,但是函數(shù) g 也沒有異常捕獲,那么再將執(zhí)行權(quán)交給函數(shù) f 對應(yīng)的棧幀。所以調(diào)用的時(shí)候棧幀一層一層創(chuàng)建,當(dāng)執(zhí)行完畢或者出現(xiàn)異常時(shí),棧幀再一層層回退。
圖片
因此棧幀的遍歷順序是從函數(shù) h 到模塊,traceback 的遍歷順序是從模塊到函數(shù) h。
為什么要執(zhí)行 del
前面說了,在 except 語句塊內(nèi),如果將異常賦給了某個(gè)變量,那么 except 結(jié)束時(shí)會將變量刪掉。
e = 2.71
def get_e():
return e
try:
raise Exception("我要引發(fā)異常了")
except Exception as e:
# 因?yàn)?except Exception as e 位于全局作用域
# 所以執(zhí)行完之后,全局變量 e 就被修改了
print(get_e()) # 我要引發(fā)異常了
# 但是在最后還會隱式地執(zhí)行 del e,那為什么要這么做呢?
# 因?yàn)?except 子句結(jié)束后,變量 e 指向的異常對象就沒用了
# 而如果不 del e 的話,那么異常對象不會被銷毀
# 此外還有一個(gè)原因,通過 __traceback__ 可以拿到當(dāng)前的回溯棧,即 traceback 對象
print(e.__traceback__) # <traceback object at 0x104a98b80>
# 而 traceback 對象保存當(dāng)前的棧幀,然后棧幀又保存了包含變量 e 的名字空間
print(e.__traceback__.tb_frame.f_locals["e"] is e) # True
# 相信你能猜到這會帶來什么后果,沒錯,就是循環(huán)引用
# 因此在 except 結(jié)束時(shí)會隱式地 del e
# 顯然當(dāng) except 結(jié)束后,全局變量 e 就無法訪問了
print(e)
"""
NameError: name 'e' is not defined
"""
所以在附加了回溯信息的情況下,它們會形成堆棧幀的循環(huán)引用,在下一次垃圾回收執(zhí)行之前,會使所有變量都保持存活。
小結(jié)
本篇文章我們就分析了異常捕獲的實(shí)現(xiàn)原理,總的來說并不難,因?yàn)樗械男畔⒍检o態(tài)保存在了異常跳轉(zhuǎn)表(簡稱異常表)中。并且在不報(bào)錯時(shí),異常捕獲對程序性能沒有任何影響,所以放心使用。