使用 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 中,例外並不一定是錯誤,例如 SystemExit
、GeneratorExit
、KeyboardInterrupt
,或者是 StopIteration
等,更像是一種事件,用來通知呼叫者,流程因為某個原因無法繼續而必須中斷。
在其他程式語言中,常會有個告誡,例外處理就應當用來處理錯誤,不應該將例外處理當成是程式流程的一部份,然而在 Python 中,就算例外是個錯誤,只要程式碼能明確表達出意圖的情況下,也常作為流程的一部份。
舉個例子來說,如果 import
的模組不存在,就會引發 ImportError
,然而 import
是個陳述句,可以出現在陳述句能出現的場合,因此有時候,會想看看某個模組能否被 import
,若模組不存在,就改 import
另一個模組,此時在 Python 中就會如此撰寫:
try:
import some_module
except ImportError:
import other_module
就程式碼流程的語義來說,也蠻符合的,也就是「嘗試 import some_module
,若引發 ImportError
就 import 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
直譯器處理,預設的處理方式是顯示一開始看到的堆疊訊息。