Python 教學都會說明例外處理機制與 try
的語法,可是你知道 try
有分『有 except
』和『沒有 except
』兩種嗎?大部分的教學說明的都是有 except
的寫法, 本文就針對沒有 except
的寫法說明它的用途。
只善後、例外留給別人處理的 try...finally
撰寫 try
時, 其實是可以完全不加任何 except
子句, 但在這種情況下, 就一定要有 finally
子句, 例如:
>>> try:
... 1/0
... finally:
... print("clean up.")
...
clean up.
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero
>>>
你可以看到實際執行的結果, 在 finally
子句內的程式會先執行, 然後再引發例外。
這是因為無論是 try...finanlly
或是 try...except..finally
的寫法, 加上 finally
子句後, 只要在 try
或是 except
子句內有未處理的例外, 這個例外就會被儲存起來, 接著執行 finally
子句內的程式, 然後再重新引發剛剛儲存的例外。
如果你的程式是要將例外交給上層處理, 但是必須進行必要的善後清理工作, 像是撰寫 API, 就很適合採用這種寫法。在 MicroPython 的 ntptime 模組中就可以看到這樣的寫法, 它並不處理與 NTP 伺服器傳輸的例外, 但是會關閉用來傳輸的 socket。
同樣的功能也可以用比較囉嗦的 try...except...finally
達成, 像是這樣:
>>> try:
... 1/0
... except:
... raise
... finally:
... print("clean up.")
...
clean up.
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero
>>>
不過這樣其實是畫蛇添足, 多寫了兩列程式, 但是實際效用和 try...finally
的寫法一樣。
在函式與迴圈中丟棄例外不處理
如果是在函式中使用 try...finally
, 可在 finally
子句中使用 return
跳離函式, 直接丟棄儲存的例外, 例如:
>>> def no_exception():
... try:
... 1/0
... finally:
... print("return from function")
... return
...
>>> no_exception()
return from function
>>>
在迴圈中使用 try...finally
也有類似的用法, 例如使用 break
也會丟棄例外跳出迴圈:
>>> for i in range(2):
... print(i)
... try:
... 1/0
... finally:
... break
...
0
>>>
或者也可以使用 continue
丟棄例外進入下一輪迴圈:
>>> for i in range(2):
... print(i)
... try:
... 1/0
... finally:
... continue
...
0
1
>>>
取得例外資訊
在 finally
中由於不像是 except
子句可以直接取得例外物件, 若需要例外的相關資訊, 可以透過 sys
模組的 exc_info()
函式取得:
>>> try:
... 1/0
... except BaseException as e:
... raise e
... finally:
... info = sys.exc_info()
...
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero
>>> info
(<class 'ZeroDivisionError'>, ZeroDivisionError('division by zero'), <traceback object at 0x00000226F07CD240>)
>>>
sys.exc_info()
會傳回元組, 內含 3 個項目, 分別是例外型別的 type
物件、例外物件以及可用來回溯例外引發過程的 traceback
物件。
我們可以透過 traceback
模組來解析 traceback
物件, 由於在互動環境下 traceback
物件的資訊比較簡略, 因此以下的範例改以完整的程式檔來示範。我們可以透過 traceback
模組的 print_exception()
印出例外物件的引發歷程:
import sys
import traceback
def func_b():
1/0 # 第 5 列
def func_a():
func_b() # 第 8 列
try:
func_a() # 第 11 列
except BaseException as e:
raise e # 第 13 列
finally:
info = sys.exc_info()
print("===print_exception=================================")
traceback.print_exception(info[0], info[1], info[2])
print("===================================================")
執行結果如下:
❯ py te.py
===print_exception=================================
Traceback (most recent call last):
File "D:\temp\te.py", line 13, in <module>
raise e
File "D:\temp\te.py", line 11, in <module>
func_a()
File "D:\temp\te.py", line 8, in func_a
func_b()
File "D:\temp\te.py", line 5, in func_b
1/0
ZeroDivisionError: division by zero
===================================================
Traceback (most recent call last):
File "D:\temp\te.py", line 13, in <module>
raise e
File "D:\temp\te.py", line 11, in <module>
func_a()
File "D:\temp\te.py", line 8, in func_a
func_b()
File "D:\temp\te.py", line 5, in func_b
1/0
ZeroDivisionError: division by zero
它的輸出結果就跟 Python 直譯器印出的結果是一樣的。它會一層一層顯示例外的引發過程:
- 第 1 層列出的是
finally
中取得的例外, 它是由第 13 列的raise e
引發的。 - 第 2 層可以看到
raise e
引發的例外物件是從第 11 列叫用func_a()
所產生。 - 第 3 層可看到叫用
func_a()
所產生的例外是來自第 8 列在func_a()
叫用func_b()
而來。 - 第 4 層可看到叫用
func_b()
引發的例外是因為第 5 列在func_b()
中執行1/0
所導致。
透過這樣的追蹤, 程式到底哪裡出錯就一清二楚了。
如果你不是要列印到畫面上, 也可以使用 traceback
模組的format_exception()
取得一行行的字串, 例如:
import sys
import traceback
def func_b():
1/0
def func_a():
func_b()
try:
func_a()
except BaseException as e:
raise e
finally:
info = sys.exc_info()
print("===format string===================================")
strs = traceback.format_exception(info[0], info[1], info[2])
for s in strs:
print(s, end="")
print("===================================================")
執行結果和前一個範例檔一樣, 要特別注意的是這個函式傳回的是字串串列, 其中每個字串都已經在結尾處加上了換行字元。
如果想要取得一層層回溯例外歷程的細部資訊, 可以改用 traceback
模組的 extract_tb()
, 它會傳回一個串列, 內含 traceback.StackSummary
物件, 個別對應到例外引發歷程的一層,可透過個別屬性取得該層的例外細部資訊, 常用的屬性如下:
屬性 | 說明 |
---|---|
filename | 引發例外的程式所在的檔案名稱 |
lineno | 引發例外的程式在檔案內的列編號 |
line | 引發例外的那一列程式內容 |
name | 引發例外的程式所在的函式名稱, 若不在函式內則是 '<module>' |
例如:
import sys
import traceback
def func_b():
1/0
def func_a():
func_b()
try:
func_a()
except BaseException as e:
raise e
finally:
info = sys.exc_info()
print("===extract_db=======================================")
summaries = traceback.extract_tb(info[2])
for fs in summaries:
print(fs.filename, fs.lineno, fs.line, fs.name)
print("===================================================")
執行結果如下:
❯ py te.py
===extract_db=======================================
D:\temp\te.py 13 raise e <module>
D:\temp\te.py 11 func_a() <module>
D:\temp\te.py 8 func_b() func_a
D:\temp\te.py 5 1/0 func_b
===================================================
Traceback (most recent call last):
File "D:\temp\te.py", line 13, in <module>
raise e
File "D:\temp\te.py", line 11, in <module>
func_a()
File "D:\temp\te.py", line 8, in func_a
func_b()
File "D:\temp\te.py", line 5, in func_b
1/0
ZeroDivisionError: division by zero
有了這些資訊後, 你就可以編排成自己喜好的顯示格式, 或是製作例外相關的工具程式了。
小結
許多人學習程式語言可能都受限於所選用的書籍或是教材, 因而略過了許多細節, 如果常常去翻一下程式語言本身的規格書, 就會有許多小驚喜, 原來程式可以這樣寫啊!