Python錯誤處理 — try與except
在程式撰寫的過程中,出現錯誤在所難免。這些錯誤大致分成幾種狀況:首先是語法上的錯誤,例如不小心將函式的名稱打錯,這類型的錯誤大部分在執行之前就會被Python找出來。另外一種狀況是程式雖然可以成功地執行,但是執行的結果卻不是我們所預期。這種計算結果不正確的錯誤叫做邏輯錯誤。另外,還有一種與撰寫的程式較無關的錯誤狀況,例如磁碟空間不足、主機無法正常運作等執行上的異常導致程式無法正常執行。
錯誤處理方式
既然程式錯誤在所難免,對於錯誤發生的處理就成了必備的一項工作。程式錯誤的處理方式有二兩種不同的風格:LBYL(三思而後行)與EAFP(先做再說,出錯再處理)。
LBYL(Look Before You Leap)三思而後行
採用LBYL風格程式語言創造者認為,應該要以防範未然的態度面對程式錯誤,能夠事前防堵就要事先進行防堵,最好可以在錯誤發生之前,儘可以能的把問題檢查出來。
C或者是Java等語言,在錯誤的例外處理上採用的是這種思維方式。在操作程式碼前,就先進行檢查,一但通過檢查之後,才執行程式。
EAFP(Easier to Ask for Forgiveness than Permission) 先做再說,出錯再處理
另一種處理方式為EAFP,採用這種處理風格者認為,事後得到寬恕比事前取得允許容易得多。並傾向於錯誤發生後再來處理,Python屬於這種處理方式。在說明文件中有記載:
Easier to ask for forgiveness than permission. This common Python coding style assumes the existence of valid keys or attributes and catches exceptions if the assumption proves false. This clean and fast style is characterized by the presence of many try and except statements. The technique contrasts with the LBYL style common to many other languages such as C.
乍看之下,可能會覺得很危險,但是若例外機制使用妥當的話,只有在錯誤發生(in case it doesn’t work)時才需要例外機制針對錯誤執行(take care of the consequences)。而且程式碼不會煩瑣且易於閱讀。
Python的例外
Python提供了不同的例外類型,比便於分別依照錯誤的狀況進行反應。在Python文件中,列出了各種的例外類型,如下圖:
從這裡可以看到,Python的例外為類別與類別之階層性的繼承架構。下層類別繼承自上層類別。例如KeyError繼承自LookupError,繼承自Exception,最終繼承自BaseException。
如果想要自己定義例外的狀況時,可以從繼承自Exception開始。例如我們可以自訂一個MyErr類別,並在裡面設計自己的例外狀況:
class MyErr(Exception):
....略...
raise拋出例外
當出現輸入資料不正確時,可以決定在各種情況下拋出不一樣的例外錯誤。Python拋出例外錯誤的方法為透過 raise 關鍵字加上例外錯誤的類別,後面接著自訂的錯誤訊息。使用raise拋出例外的方式如下,我們將上面的例2改寫:
def show_fruit(pos):
fruits = ['apple','banana','cherry']
if pos >= len(fruits):
**raise IndexError("該水果不存在")**
print(f'您選的是:{fruits[pos]}')
try:
f = int(input("請選擇水果: "))
show_fruit(f)
except IndexError as err:
print(f"發生Index問題:{err}")
except Exception as other:
print(f"發生其他問題:{other}")
執行程式時,當我們輸入1選項時,會得到正確的反應。
可是,當我們選擇輸入3的時候,就會出現IndexError錯誤,並且顯示『該水果不存在』的提示。由於透過raise拋出例外時,若給定第一個參數的話,通常會假定這個參數是要作為錯誤訊息顯示給使用者。
需要注意的地方是,當自行拋出例外錯誤時,在呼叫的那一端一定要作例外處理,才會顯示出自訂的錯誤訊息。
assertion斷言偵測錯誤
斷言可以用來自動偵測Python程式碼,以確保程式不會有什麼明顯的錯誤,是個除錯的輔助工具。使用的是asset陳述句來執行檢查,它是raise的一種特殊形式。其語法如下:
assert 運算式 , 參數
簡言之,就是『我斷定(下面)這個條件為真(True),否則(False)這中間一定出了什麼問題』。以就是說,如果程式判定為真(True),那就表示程式正常沒問題,可以繼續按計劃執行;反之一旦判定為假(False)的時候,程式就會引發AssertionError異常而停止當掉。
舉例來說,我們需要一個函式(how_many_people)給人們輸入人數,這時候就可以增加一個斷言來確保輸入的num數字不會是負數,畢竟真實的世界不會有-1個人這種情形。
def how_many_people(num):
people = int(num)
assert 0 <= people
return people
如果硬要輸入-1的話,就會出現AssertionError異常。
斷言的方式,主要是針對設計師可能犯下的錯誤來進行的內部檢查提示,而不是針對應用程式的使用者所犯下的錯誤來進行提示的。如果這些斷言條件出現了False,就表示程式裡面有bug需要修正。如果是使用者的錯誤,這種錯誤大都可以被恢復,例如輸入不合法的錯誤資料,應該使用try與except來處理。
Python使用try與except處理錯誤
Python使用「例外處理」的方式指出錯誤,當錯誤發生時,就會執行與它有關的程式。使用的關鍵字為try、except,用以捕獲與處理例外。
我們可以在任何可能發生例外的地方添加例外處理程式。雖然採用這種方式可能無法修復問題,但至少可以提示目前程式處理的狀況並且優雅地關閉程式。
我們可以看看下面的例子(例1):
fruit = ['apple','banana','cherry']
pos = 6
fruit[pos]
如果執行上面的程式碼,會得到一個IndexError錯誤(IndexError: list index out of range)。
從這裡看來,當程式中出現錯誤或者是例外異常時,整個程式就會全部當掉出錯。在這樣的情況看來,並不是很優雅,我們希望程式可以偵測出錯誤,發出提示並且繼續執行其他的部分,該如何處理?
這個時候,如果使用Python例外處理的方式指出錯誤,結果會有何不同?
我們可以把上面的例子(例1)改寫為:
fruit = ['apple','banana','cherry']
pos = 6
try:
fruit[pos]
except:
print(f"pos的值必須為0到2,目前得到的是{pos}")
執行上面的程式碼,我們可以看到顯然程式執行到了except的部分,打印出我們在程式碼中預設的結果。
如果把pos改為2(pos = 2
)之後再執行一次。結果如下,會得到cherry。
從上面的例1可以看出,Python程式會先執行try裡面的程式區塊,如果發生錯誤,就會發出例外,並且執行例外部分的程式區塊;反之,如果一切正常,就會跳過例外部分的執行。
因此,我們可以將有可能出錯的程式碼,放到try裡面,當錯誤發生時,程式的執行就會跳到接下來的except部分開始,執行裡面的程式碼,然後又繼續執行後面的程式,不會當掉。
多個except例外
上面的使用方式,不在except後面加引數時,可以捕捉到任何型態的錯誤。但是這種處理方式可以說是很籠統。若要更精確的處理例外的話,我們可以針對不同的例外類型,分別提供不同的例外回應方式。
想得知例外的細節,可以使用下面的陳述方式:
except 例外類型 as 例外名稱
為了讓程式碼簡潔,可以在例外錯誤類別之後利用 as 關鍵字,另取別名,例如IndexError as err
以及Exception as other
。
下面的例子(例2),我們把例1改寫如下:
fruit = ['apple','banana','cherry']
pos = 6
try:
fruit[pos]
except IndexError as err:
print(f"發生Index問題:{err}")
except Exception as other:
print(f"發生其他問題:{other}")
裡面有兩個except,其中第一個except是IndexError。依照前面的陳述方式,except 例外類型 as 例外名稱,IndexError是例外類型,而err是例外的名稱);另外一個則是泛用的Exception例外,other是例外的名稱。
執行程式碼之後,會得到一個『發生Index問題』的錯誤提示。
如果把程式碼裡面的pos值改為one的話(程式碼如下),其他部分不變:
fruit = ['apple','banana','cherry']
pos = 'one'
try:
fruit[pos]
except IndexError as err:
print(f"發生Index問題:{err}")
except Exception as other:
print(f"發生其他問題:{other}")
執行程式碼後,則會得到一個『發生其他問題』的錯誤提示。
從上面的測試可以看到,當出現位置錯誤時,會拋出的是IndexError的例外,而當錯誤發生在IndexError以外的其他地方時,則會拋出Exception錯誤。
在例外發生時,當程式碼在不同的例外錯誤項目中進行except配對時,其順序是由上而下,只要出現配對成功的例外,剩下的例外就算配對到了也不會執行,所以執行結果只顯示一則例外的訊息。
Exception,可以捕捉除了系統例外以外的所有例外。而若在except陳述句的後面不接上任何例外類型,表示這裡要捕捉所有例外,如此一來包括所有系統的例外都會捕捉進來。特別注意的是,Exception要放在其他例外的後面,否則其他例外都會被它補獲,而導致設定的其他例外都成無用武之地。
因此,我們可以針對不同的例外類型,預先定義在程式碼裡面,以分別提供不同的例外處理方式。
程式撰寫出現錯誤在所難免。工程師能在撰寫過程中處理錯誤,就成了必備的能力。因此我們在學習Python的時候,需要暸解如何解讀錯誤訊息,以及如何在程式的中間設定例外處理方式,才能讓寫出來的應用程式經得起實際使用的考驗。