例外處理(Exception)

程式執行時發生的異常狀況稱為『例外(exception)』。程式錯誤可分以下幾類:

傳統錯誤處理與例外處理的差異

  • 忽略此錯誤

  • 所有函數皆傳回成功/失敗的狀況–LBYL(Look Before You Leap,三思而後行)

EX:

  • 例外處理–EAPL(Easier to Ask Forgiveness than Permission,取得事後寬恕比事先得到許可要更容易)

    • 上述範例中,有太多重複的程式碼:每次嘗試寫入檔案時檢查錯誤,並在檢查到錯誤時將錯誤狀態回傳。

    • 而硬碟空間錯誤只有在最上層save_to_file()處理,故大多數的錯誤處理其實只是將錯誤回傳至上一層,並一層一層往上傳遞。

    • 例外處理:可避免上述自行建立的傳遞管道。並寫出類似以下的程式碼:

  • 產生例外的行為稱為引發(raise)或是拋出(throw)例外。

  • 取得例外的行為稱為捕獲(catch)例外。

  • 處理例外的程式碼稱為例外處理程式碼(exception-handling code)或是例外處理程序(exception handler)。

  • Python與大多數具有例外處理機制的程式語言一樣,為不同類型的問題定義相對應的例外。

  • 發生的事件不同,就會引發不同類型的例外。

  • 例外處理程序則可設定只處理特定類型的例外。

Python的例外類型

  • 以下為Python3提供的多種例外類型:

  • Python的例外是以『階層式』架構呈現。

  • 每種例外類型都是Python的類別,它繼承自上述階層架構的上一層類別。

  • 大多數的例外都是繼承自Exception,故建議使用者自定的例外也繼承Exception:因為Ctrl+C可中斷try區塊中的程式碼,而不會觸發例外處理。

    • 因為KeybordInterrupt例外不屬於Exception的子類別

引發例外

  • 大部分Python內建函數都會引發例外:
alist = [1, 2, 3]
# element = alist[7]
# IndexError: list index out of range
  • 例外可由raise敘述來引發,其基本語法如下:
raise 例外類型(參數)  ## 依照例外類型的不同,參數型別與數量也有所不同
# raise IndexError("Just Testing")   ## 自定錯誤訊息
# IndexError: Just Testing

例外的捕獲與處理

  • 例外處理機制的好處為它們不必導致程式終止,透過定義適當的例外處理程序,則可確保常見的例外情況而不會導致程式執行失敗。

  • 例外處理程序亦可向使用者顯示錯誤訊息或做其他事情,甚至可以解決例外的問題後讓程式繼續運行。

  • Python採try、except、else關鍵字來捕獲與處理例外,其基本語法如下

  • try敘述首先會執行程式主體區塊的程式碼,如果執行成功,則執行else區塊的程式碼

  • 若是有finally子句,最後還會執行finally區塊程式碼。

  • 如果程式主體區塊拋出例外,則會往下依序搜尋except子句,比對被拋出的例外與except預期的類型是否一致。

  • 如果找到匹配的except子句,則會把被拋出的例外設定為as後面的變數,並執行該子句的程式區塊。

  • 若無『as變數』,仍然可以捕獲指定類型的例外,只是不會把拋出的例外設定給任何變數。

  • 最後一個沒有指定類型的except子句可以處理所有類型的例外(可有可無)。但建議不要這樣做。因為所有的錯誤都會被隱藏至這個子句裡面,進而產生某些令人困惑的行為。

  • 如果最後沒有任何except子句可以處理例外,則會將該例外從本層函數往上層拋出,直到有其他try敘述能處理它。

  • 若有finally子句,則無論有沒有發生例外,都會執行finally區塊。即使例外沒有任何except子句能夠處理,也會在finally區塊執行完畢後再往上拋出該例外。

  • 因finally區塊永遠都會被執行,故可在該區塊定義清理用之程式碼。如關閉檔案,斷開資料庫等。

定義新的例外

class MyError(Exception):
  pass
# raise MyError("發生XXX錯誤")
# MyError: 發生XXX錯誤
try:
  raise MyError("發生XXX錯誤")
except MyError as error:
  print("狀況:", error)
## 狀況: 發生XXX錯誤
  • 如raise引發時給予多個參數,這些參數將以tuple形式傳遞至處理程序,可透過該例外物件的args屬性來存取:
try:
  raise MyError("寫入錯誤", "my_filename", 3)
except MyError as error:
  #error.args[0], error.args[1], error.args[2]
  #print(error)
  print("狀況:檔案{1} 發生 {0}\n錯誤碼:{2}".format(error.args[0], error.args[1], error.args[2]))
## 狀況:檔案my_filename 發生 寫入錯誤
## 錯誤碼:3

assert敘述為程式除錯

  • assert 運算式, 參數
x = [1, 2, 3]
# assert len(x) > 5, "發生錯誤, len(x)小於等於5"
# AssertionError: 發生錯誤, len(x)小於等於5
  • assert 運算式等同於以下程式碼:
if __debug__:
  if not 運算式:
     raise AssertionError(參數)

例外繼承的階層關係

  • 如果程式主體拋出IndexError,因為IndexError是繼承自LookupError的子類別,所以第一個except子句會捕捉到IndexError,導致第二個except子句永遠不會被用到。

  • 故應該兩個except子句順序對調。