處理例外狀況

已完成

當您第一次發現將大型追蹤顯示為輸出的例外狀況時,您可能會想要攔截每個錯誤,以避免發生該錯誤。

如果您在 Mars 的任務中,若導覽系統上的文字顯示「發生錯誤」,您該怎麼做? 假設沒有其他資訊或內容,只有閃爍的紅燈與錯誤文字。 身為開發人員,將自己放在程式的另一端很有用:發生錯誤時,使用者可以做什麼?

雖然本課程模組涵蓋如何藉由攔截例外狀況來進行處理,但不需要隨時攔截例外狀況。 有時候,讓例外狀況引發很有用,其他呼叫者即可處理錯誤。

嘗試和例外區塊

讓我們使用導覽器範例來建立程式碼,以開啟 Mars 任務的組態檔。 組態檔可能有各種問題,因此當問題出現時,精確回報問題非常重要。 我們知道,如果檔案或目錄不存在,就會引發 FileNotFoundError。 如果我們想要處理該例外狀況,可以使用 tryexcept 區塊來執行該作業:

try:
     open('config.txt')
except FileNotFoundError:
     print("Couldn't find the config.txt file!")

Couldn't find the config.txt file!

try 關鍵字之後,您會新增可能造成例外狀況的程式碼。 接下來,您會新增 except 關鍵字以及可能的例外狀況,後面接著任何需要在該情況發生時執行的程式碼。 因為 config.txt 不存在於系統中,所以 Python 會列印組態檔不存在。 tryexcept 區塊以及有用的訊息會防止追蹤,但仍會通知使用者相關問題。

雖然不存在的檔案很常見,但這不是您可能發現的唯一錯誤。 無效的檔案權限可能會防止讀取檔案,即使檔案存在也一樣。 讓我們在 Visual Studio Code 中建立名為 config.py 的新 Python 檔案。 將下列程式碼新增至尋找並讀取導覽系統的組態檔的檔案:

def main():
    try:
        configuration = open('config.txt')
    except FileNotFoundError:
        print("Couldn't find the config.txt file!")


if __name__ == '__main__':
    main()

接著,建立名為 config.txt目錄。 嘗試呼叫 config.py 檔案,以查看應該與此類似的新錯誤:

python3 config.py
Traceback (most recent call last):
  File "/tmp/config.py", line 9, in <module>
    main()
  File "/tmp/config.py", line 3, in main
    configuration = open('config.txt')
IsADirectoryError: [Errno 21] Is a directory: 'config.txt'

處理此錯誤的沒用方法會是攔截所有可能的例外狀況,以防止追蹤。 如需了解擷取所有例外狀況的原因,請藉由更新 config.py 檔案中新建立的 main() 函式來嘗試:

def main():
    try:
        configuration = open('config.txt')
    except Exception:
        print("Couldn't find the config.txt file!")

現在,在 config.txt 目錄所在且具有不當權限的相同位置,再次執行程式碼:

python3 config.py
Couldn't find the config.txt file!

現在問題在於錯誤訊息不正確。 目錄確實存在,但其具有不同的權限,Python 無法予以讀取。 當您處理軟體錯誤時,有下列錯誤可能會使人沮喪:

  • 未指出真正的問題為何。
  • 提供不符合實際問題的輸出。
  • 未提示可採取哪些動作來修正問題。

讓我們修正這一段程式碼,以解決上述所有挫折。 恢復為攔截 FileNotFoundError ,然後新增另一個 except 區塊以攔截 PermissionError

def main():
    try:
        configuration = open('config.txt')
    except FileNotFoundError:
        print("Couldn't find the config.txt file!")
    except IsADirectoryError:
        print("Found config.txt but it is a directory, couldn't read it")

現在,在 config.txt 目錄所在的同一位置再次加以執行:

python3 config.py
Found config.txt but couldn't read it

現在刪除 config.txt 目錄,以確保會改為觸達第一個 except 區塊:

 rm -f config.txt
 python3 config.py
Couldn't find the config.txt file!

當錯誤屬於類似的本質,而且不需要個別進行處理時,您可以在 except 行中使用括弧,將例外狀況群集在一起。 例如,如果導覽系統負載沉重,而且檔案系統變得太忙碌,則一起攔截 BlockingIOErrorTimeOutError 會很合理:

def main():
    try:
        configuration = open('config.txt')
    except FileNotFoundError:
        print("Couldn't find the config.txt file!")
    except IsADirectoryError:
        print("Found config.txt but it is a directory, couldn't read it")
    except (BlockingIOError, TimeoutError):
        print("Filesystem under heavy load, can't complete reading configuration file")

提示

雖然您可以將例外狀況群集在一起,但只有在不需要個別處理例外狀況時,才這麼做。 避免將許多例外狀況群集在一起,以提供一般化錯誤訊息。

如果您需要存取與例外狀況相關聯的錯誤,您必須更新 except 行以包含 as 關鍵字。 如果例外狀況太過普通,則錯誤訊息可能很有用:

try:
    open("mars.jpg")
except FileNotFoundError as err:
     print("Got a problem trying to read the file:", err)
Got a problem trying to read the file: [Errno 2] No such file or directory: 'mars.jpg'

在此情況下,as err 表示 err 變成以例外狀況物件作為值的變數。 然後,它會使用此值來列印與例外狀況相關聯的錯誤訊息。 使用此技巧的另一個原因是直接存取錯誤的屬性。 例如,如果您攔截的是更為普通的 OSError 例外狀況,也就是 FilenotFoundErrorPermissionError父代例外狀況,您可經由 .errno 屬性來分辨它們:

try:
    open("config.txt")
except OSError as err:
     if err.errno == 2:
         print("Couldn't find the config.txt file!")
     elif err.errno == 13:
        print("Found config.txt but couldn't read it")
Couldn't find the config.txt file!

一律嘗試使用可為程式碼提供最佳可讀性並協助未來維護的技巧。 有時候,必須使用較不易讀取的程式碼,以在錯誤發生時提供更好的使用者體驗。