###### tags: `Python` # 例外最終還是例外--沒有 except 的 try Python 教學都會說明[例外處理機制](https://docs.python.org/3/reference/executionmodel.html#exceptions)與 [`try`](https://docs.python.org/3/reference/compound_stmts.html#the-try-statement) 的語法,可是你知道 `try` 有分『有 `except`』和『沒有 `except`』兩種嗎?大部分的教學說明的都是有 `except` 的寫法, 本文就針對沒有 `except` 的寫法說明它的用途。 ## 只善後、例外留給別人處理的 `try...finally` 撰寫 `try` 時, 其實是可以完全不加任何 `except` 子句, 但在這種情況下, 就一定要有 `finally` 子句, 例如: ```python >>> 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](https://github.com/micropython/micropython/blob/4d9e657f0ee881f4a41093ab89ec91d03613744d/ports/esp8266/modules/ntptime.py#L22) 模組中就可以看到這樣的寫法, 它並不處理與 NTP 伺服器傳輸的例外, 但是會關閉用來傳輸的 socket。 同樣的功能也可以用比較囉嗦的 `try...except...finally` 達成, 像是這樣: ```python >>> 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` 跳離函式, 直接丟棄儲存的例外, 例如: ```python >>> def no_exception(): ... try: ... 1/0 ... finally: ... print("return from function") ... return ... >>> no_exception() return from function >>> ``` 在迴圈中使用 `try...finally` 也有類似的用法, 例如使用 `break` 也會丟棄例外跳出迴圈: ```python >>> for i in range(2): ... print(i) ... try: ... 1/0 ... finally: ... break ... 0 >>> ``` 或者也可以使用 `continue` 丟棄例外進入下一輪迴圈: ```python >>> for i in range(2): ... print(i) ... try: ... 1/0 ... finally: ... continue ... 0 1 >>> ``` ## 取得例外資訊 在 `finally` 中由於不像是 `except` 子句可以直接取得例外物件, 若需要例外的相關資訊, 可以透過 `sys` 模組的 [`exc_info()`](https://docs.python.org/3/library/sys.html#sys.exc_info) 函式取得: ```python >>> 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`](https://docs.python.org/3/reference/datamodel.html#traceback-objects) 物件。 我們可以透過 [`traceback` 模組](https://docs.python.org/3/library/traceback.html)來解析 `traceback` 物件, 由於在互動環境下 `traceback` 物件的資訊比較簡略, 因此以下的範例改以完整的程式檔來示範。我們可以透過 `traceback` 模組的 [`print_exception()`](https://docs.python.org/3/library/traceback.html#traceback.print_exception) 印出例外物件的引發歷程: ```python 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("===================================================") ``` 執行結果如下: ```python ❯ 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. 第 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()`](https://docs.python.org/3/library/traceback.html#traceback.format_exception) 取得一行行的字串, 例如: ```python 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()`](https://docs.python.org/3/library/traceback.html#traceback.extract_tb), 它會傳回一個串列, 內含 [`traceback.StackSummary` 物件](https://docs.python.org/3/library/traceback.html#stacksummary-objects), 個別對應到例外引發歷程的一層,可透過個別屬性取得該層的例外細部資訊, 常用的屬性如下: |屬性|說明| |---|---| |filename|引發例外的程式所在的檔案名稱| |lineno|引發例外的程式在檔案內的列編號| |line|引發例外的那一列程式內容| |name|引發例外的程式所在的函式名稱, 若不在函式內則是 '\<module\>'| 例如: ```python 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("===================================================") ``` 執行結果如下: ```python ❯ 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 ``` 有了這些資訊後, 你就可以編排成自己喜好的顯示格式, 或是製作例外相關的工具程式了。 ## 小結 許多人學習程式語言可能都受限於所選用的書籍或是教材, 因而略過了許多細節, 如果常常去翻一下程式語言本身的規格書, 就會有許多小驚喜, 原來程式可以這樣寫啊!