上一篇文章我們介紹了 Python 的異常是怎么實(shí)現(xiàn)的,拋出異常這個(gè)動作在虛擬機(jī)層面上是怎樣的一個(gè)行為,以及虛擬機(jī)在處理異常時(shí)候的棧幀展開行為。 既然虛擬機(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)錯(cuò),因?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é)碼入手。 隨便寫一段代碼,然后反編譯一下。 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)錯(cuò)誤而拋出異常,另一種是使用 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 子句的指令如下。 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)到什么地方了。 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ì)有人會感到困惑。 這幾條指令的具體作用,我們稍后解釋。 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í)行出錯(cuò),跳轉(zhuǎn)到偏移量為 66 的指令去清理異常。 30 to 44 -> 56 偏移量為 30 ~ 44 的指令對應(yīng) except 子句內(nèi)部的處理邏輯,如果執(zhí)行出錯(cuò)則跳轉(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)錯(cuò)了呢?其實(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)錯(cuò)誤時(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ā)生錯(cuò)誤是因?yàn)樵诘?10 行調(diào)用了 f() File "/Users/.../main.py", line 10, in <module> f()
# 打印函數(shù) f 的 traceback # 并提示:發(fā)生錯(cuò)誤是因?yàn)樵诘?8 行調(diào)用了 g() File "/Users/.../main.py", line 8, in f g()
# 打印函數(shù) g 的 traceback # 并提示:發(fā)生錯(cuò)誤是因?yàn)樵诘?5 行調(diào)用了 h() File "/Users/.../main.py", line 5, in g h()
# 打印函數(shù) h 的 traceback # 并提示:發(fā)生錯(cuò)誤是因?yàn)樵诘?2 行執(zhí)行了 1 / 0 File "/Users/.../main.py", line 2, in h 1 / 0
# 函數(shù) h 的 traceback -> tb_next 為 None,證明錯(cuò)誤是發(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í)行出錯(cuò)了,但又沒有異常捕獲,那么會將執(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。 前面說了,在 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)?nbsp;except Exception as e 位于全局作用域 # 所以執(zhí)行完之后,全局變量 e 就被修改了 print(get_e()) # 我要引發(fā)異常了 # 但是在最后還會隱式地執(zhí)行 del e,那為什么要這么做呢? # 因?yàn)?nbsp;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 # 相信你能猜到這會帶來什么后果,沒錯(cuò),就是循環(huán)引用 # 因此在 except 結(jié)束時(shí)會隱式地 del e
# 顯然當(dāng) except 結(jié)束后,全局變量 e 就無法訪問了 print(e) """ NameError: name 'e' is not defined """
所以在附加了回溯信息的情況下,它們會形成堆棧幀的循環(huán)引用,在下一次垃圾回收執(zhí)行之前,會使所有變量都保持存活。 本篇文章我們就分析了異常捕獲的實(shí)現(xiàn)原理,總的來說并不難,因?yàn)樗械男畔⒍检o態(tài)保存在了異常跳轉(zhuǎn)表(簡稱異常表)中。并且在不報(bào)錯(cuò)時(shí),異常捕獲對程序性能沒有任何影響,所以放心使用。
|