DEV Community

codemee
codemee

Posted on

例外最終還是例外--沒有 except 的 try

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
>>>
Enter fullscreen mode Exit fullscreen mode

你可以看到實際執行的結果, 在 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
>>>
Enter fullscreen mode Exit fullscreen mode

不過這樣其實是畫蛇添足, 多寫了兩列程式, 但是實際效用和 try...finally 的寫法一樣。

在函式與迴圈中丟棄例外不處理

如果是在函式中使用 try...finally, 可在 finally 子句中使用 return 跳離函式, 直接丟棄儲存的例外, 例如:

>>> def no_exception():
...     try:
...         1/0
...     finally:
...         print("return from function")
...         return
...
>>> no_exception()
return from function
>>>
Enter fullscreen mode Exit fullscreen mode

在迴圈中使用 try...finally 也有類似的用法, 例如使用 break 也會丟棄例外跳出迴圈:

>>> for i in range(2):
...     print(i)
...     try:
...         1/0
...     finally:
...         break
...
0
>>>
Enter fullscreen mode Exit fullscreen mode

或者也可以使用 continue 丟棄例外進入下一輪迴圈:

>>> for i in range(2):
...     print(i)
...     try:
...         1/0
...     finally:
...         continue
...
0
1
>>>
Enter fullscreen mode Exit fullscreen mode

取得例外資訊

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>)
>>>
Enter fullscreen mode Exit fullscreen mode

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("===================================================")
Enter fullscreen mode Exit fullscreen mode

執行結果如下:

 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
Enter fullscreen mode Exit fullscreen mode

它的輸出結果就跟 Python 直譯器印出的結果是一樣的。它會一層一層顯示例外的引發過程:

  1. 第 1 層列出的是 finally 中取得的例外, 它是由第 13 列的raise e 引發的。
  2. 第 2 層可以看到 raise e 引發的例外物件是從第 11 列叫用 func_a() 所產生。
  3. 第 3 層可看到叫用 func_a() 所產生的例外是來自第 8 列在 func_a() 叫用 func_b() 而來。
  4. 第 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("===================================================")
Enter fullscreen mode Exit fullscreen mode

執行結果和前一個範例檔一樣, 要特別注意的是這個函式傳回的是字串串列, 其中每個字串都已經在結尾處加上了換行字元。

如果想要取得一層層回溯例外歷程的細部資訊, 可以改用 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("===================================================")
Enter fullscreen mode Exit fullscreen mode

執行結果如下:

 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
Enter fullscreen mode Exit fullscreen mode

有了這些資訊後, 你就可以編排成自己喜好的顯示格式, 或是製作例外相關的工具程式了。

小結

許多人學習程式語言可能都受限於所選用的書籍或是教材, 因而略過了許多細節, 如果常常去翻一下程式語言本身的規格書, 就會有許多小驚喜, 原來程式可以這樣寫啊!

Top comments (0)