使用 try、except

April 28, 2022

來看一個簡單的程式,使用者可以連續輸入整數,最後輸入結束後會顯示輸入數的平均值:

numbers = input('輸入數字(空白區隔):').split(' ')
print('平均', sum(int(number) for number in numbers) / len(numbers))

呼叫堆疊

如果使用者正確地輸入每個整數,程式會如預期地顯示平均:

輸入數字(空白區隔):10 20 30 40
平均 25.0 

如果使用者不小心輸入錯誤,那就會出現奇怪的訊息,例如第三個數輸入為 3o,而不是 30 了:

輸入數字(空白區隔):10 20 3o 40
Traceback (most recent call last):
  File "C:/workspace/average.py", line 2, in <module>
    print('平均', sum((int(number) for number in numbers)) / len(numbers))
  File "C:/workspace/average.py", line 2, in <genexpr>
    print('平均', sum((int(number) for number in numbers)) / len(numbers))
ValueError: invalid literal for int() with base 10: '3o'

這段錯誤訊息對除錯是很有價值的,不過先看到錯誤訊息的最後一行:

ValueError: invalid literal for int() with base 10: '3o'

問題的來源在於 int 接受了字串 '3o',無法將這樣的字串剖析為整數,在不指定基數的情況下,int()預期的字串,必須是代表十進位整數。

至於其他的錯誤訊息,顯示的程式執行的呼叫堆疊(call stack),簡單來說,就是執行的順序,訊息 File "C:/workspace/average.py", line 2, in <module> 表明了在 average 模組的第二行發生錯誤,訊息 File "C:/workspace/average.py", line 2, in <genexpr> 進一步表示了在程式碼第二行的產生器發生了錯誤。

try/except

如果你想處理 ValueError,可以在 try 區塊中撰寫可能發生錯誤的程式碼,表示試著執行該區塊的內容,若真的發生了錯誤,對應的 except 可以用來捕捉錯誤:

try:
    numbers = input('輸入數字(空白區隔):').split(' ')
    print('平均', sum(int(number) for number in numbers) / len(numbers))
except ValueError as err:
    print(err)

python 直譯器嘗試執行 try 區塊的程式碼,如果發生例外,執行流程會跳離例外發生點,然後比對 except 宣告的型態,是否符合引發的例外物件型態,如果是的話,就執行 except 區塊中的程式碼。

一個執行無誤的範例如下所示:

輸入數字(空白區隔):10 20 30 40
平均 25.0

如果呼叫 int 時發生 ValueError,流程會跳離當時的執行點,若在 except 處比對到與例外相同的型態,會執行對應的 except 區塊,由於之後沒有其他程式碼,程式就結束了。一個執行時輸入有誤的範例如下所示:

輸入數字(空白區隔):10 20 3o 40
invalid literal for int() with base 10: '3o'

呼叫 int 時若遇到無法剖析為整數的字串時,它無法執行程式流程,因而以例外的方式讓呼叫的客戶端得知,發生了無法執行程式流程的錯誤。

Python 例外風格

在 Python 中,例外不一定是錯誤,不少例外代表著一種通知,例如 for in 語法底層就運用了例外處理機制。

實際上,只要是 iterable 物件,也就是具有 __iter__ 方法的物件,都能使用 for in 迭代。__iter__ 方法必須傳回迭代器(Iterator),該迭代器具有 __next__ 方法,每次迭代時會傳回下一個元素,若沒有下一個元素,要引發 StopIteration 例外,通知客戶端沒有下一個可迭代的物件,迭代流程無法繼續。

可以使用 iter 呼叫物件的 __iter__ 取得迭代器,使用 next 呼叫迭代器的 __next__ 方法。例如:

>>> iterator = iter([10, 20, 30])
>>> next(iterator)
10
>>> next(iterator)
20
>>> next(iterator)
30
>>> next(iterator)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>

for in 在遇到 StopIteration 時,會靜靜地結束迭代,因此不會看到 StopIteration,如果自行使用函式來實作比擬,流程會像是:

def for_in(iterable, consume):
    iterator = iter(iterable)
    try:
        while True:
            consume(next(iterator))
    except StopIteration:
        pass

for_in([10, 20, 30], print)

在這邊看到 except 比對到 StopIteration 之後,並沒有使用 as,這是因為在發生 StopIteration 時,不用做什麼事,也就不必使用 as 將例外物件指定給某名稱,只要靜靜地 pass 就可以了。

在 Python 中,例外並不一定是錯誤,例如 SystemExitGeneratorExitKeyboardInterrupt,或者是 StopIteration 等,更像是一種事件,用來通知呼叫者,流程因為某個原因無法繼續而必須中斷。

在其他程式語言中,常會有個告誡,例外處理就應當用來處理錯誤,不應該將例外處理當成是程式流程的一部份,然而在 Python 中,就算例外是個錯誤,只要程式碼能明確表達出意圖的情況下,也常作為流程的一部份。

舉個例子來說,如果 import 的模組不存在,就會引發 ImportError,然而 import 是個陳述句,可以出現在陳述句能出現的場合,因此有時候,會想看看某個模組能否被 import,若模組不存在,就改 import 另一個模組,此時在 Python 中就會如此撰寫:

try:
  import some_module
except ImportError:
  import other_module

就程式碼流程的語義來說,也蠻符合的,也就是「嘗試 import some_module,若引發 ImportErrorimport other_module」。

例外的比對

except 右方可以使用 tuple 指定多個物件,也可以有多個 except,如果沒有指定 except 後的物件型態,表示全數捕捉。舉例來說,底下的範例中若使用者於 time.sleep(10) 期間,輸入 Ctrl+C 會引發 KeyboardInterrupt,若在 input 等待使用者輸入期間輸入 Ctrl+Z 會引發 EOFError。下例中處理這些可能的狀況:

import time

try:
    time.sleep(10) # 模擬一個耗時流程
    num = int(input('輸入整數:'))
    print('{0}{1}'.format(num, '奇數' if num % 2 else '偶數'))
except ValueError:
    print('請輸入阿拉伯數字')
except (EOFError, KeyboardInterrupt):
    print('使用者中斷程式')
except:
    print('其他程式例外')

當程式中發生例外時,流程會從例外發生處中斷,並進行 except 的比對,如果有相符的例外型態,就會執行對應的 except 區塊,執行完畢後若仍有後續流程,就會繼續執行。例如:

total = 0
count = 0

while number_str := input('輸入數字(直接 Enter 結束):'):
    try:
        number = int(number_str)
        total += number
        count += 1
    except ValueError as err:
        print('非整數的輸入', number_str)

print('平均', total / count) 

在這個範例中,若輸入非整數的字串,會引發 ValueError,在執行了對應的 except 區塊後流程繼續,由於仍在 while迴圈中,因此使用者仍可進行下一個輸入。一個執行範例如下:

輸入數字(直接 Enter 結束):10
輸入數字(直接 Enter 結束):20
輸入數字(直接 Enter 結束):3o
非整數的輸入 3o
輸入數字(直接 Enter 結束):30
輸入數字(直接 Enter 結束):40
輸入數字(直接 Enter 結束):
平均 25.0

如果沒有相符的例外型態,或者例外沒有使用 try..except 處理,例外會往上層呼叫者傳播,在每一層呼叫處中斷,這是運用例外機制的目的之一,就算是底層引發的例外,不需要在原始碼層面上處理,頂層的呼叫者也能獲取例外通知,決定是否進一步處理。

若引發的例外都沒有任何處理,例外傳播至最頂層,就會由 python 直譯器處理,預設的處理方式是顯示一開始看到的堆疊訊息。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter