例外最終還是例外--沒有 except 的 try
Tags: python
Published: 2022-02-16 14:42:05

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
```

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

## 小結

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