C語言中文網 目錄

Python try except else(異常處理)用法詳解

Python 的異常處理機制可以讓程序具有極好的容錯性,讓程序更加健壯。當程序運行出現意外情況時,系統會自動生成一個 Error 對象來通知程序,從而實現將“業務實現代碼”和“錯誤處理代碼”分離,提供更好的可讀性。

使用try...except捕獲異常

前面章節講過,希望有一個非常強大的“if 塊”,可以表示所有的錯誤情況,讓程序一次處理所有的錯誤,也就是希望將錯誤集中處理。

出于這種考慮,此處試圖把“錯誤處理代碼”從“業務實現代碼”中分離出來。將上面最后一段偽碼改為如下偽碼:

if 一切正常:
    #業務實現代碼
else:
    alert 輸入不合法
    goto retry

上面代碼中的“if 塊”依然不可表示,因為一切正常是很抽象的,無法轉換為計算機可識別的代碼。在這種情形下,Python 提出了一種假設,如果程序可以順利完成,那就“一切正常”,把系統的業務實現代碼放在 try 塊中定義,把所有的異常處理邏輯放在 except 塊中進行處理。

下面是 Python 異常處理機制的語法結構:

try:
    #業務實現代碼
    ...
except (Error1, Error2, ...) as e:
    alert 輸入不合法
    goto retry

如果在執行 try 塊里的業務邏輯代碼時出現異常,系統自動生成一個異常對象,該異常對象被提交給 Python 解釋器,這個過程被稱為引發異常

當 Python 解釋器收到異常對象時,會尋找能處理該異常對象的 except 塊,如果找到合適的 except 塊,則把該異常對象交給該 except 塊處理,這個過程被稱為捕獲異常。如果 Python 解釋器找不到捕獲異常的 except 塊,則運行時環境終止,Python 解釋器也將退出。

不管程序代碼塊是否處于 try 塊中,甚至包括 except 塊中的代碼,只要執行該代碼塊時出現了異常,系統總會自動生成一個 Error 對象。如果程序沒有為這段代碼定義任何的 except 塊,則 Python 解釋器無法找到處理該異常的 except 塊,程序就在此退出,這就是前面看到的例子程序在遇到異常時退出的情形。

下面使用異常處理機制來改寫前面章節中五子棋游戲中用戶下棋部分的代碼:
inputStr = input("請輸入您下棋的坐標,應以x,y的格式:\n")
while inputStr != None :
    try:
        # 將用戶輸入的字符串以逗號(,)作為分隔符,分隔成2個字符串
        x_str, y_str = inputStr.split(sep = ",")
        # 如果要下棋的點不為空
        if board[int(y_str) - 1][int(x_str) - 1] != "╋":
            inputStr = input("您輸入的坐標點已有棋子了,請重新輸入\n")
            continue
        # 把對應的列表元素賦為"●"。
        board[int(y_str) - 1][int(x_str) - 1] = "●"
    except Exception:
        inputStr = input("您輸入的坐標不合法,請重新輸入,下棋坐標應以x,y的格式\n")
        continue
    ...
上面程序把處理用戶輸入字符串的代碼都放在 try 塊里執行,只要用戶輸入的字符串不是有效的坐標值(包括字母不能正確解析,沒有逗號不能正確解析,解析出來的坐標引起數組越界……),系統就將引發一個異常對象,并把這個異常對象交給對應的 except 塊處理。

except 塊的處理方式是向用戶提示坐標不合法,然后使用 continue 忽略本次循環剩下的代碼,開始執行下一次循環。這就保證了該五子棋游戲有足夠的容錯性,即用戶可以隨意輸入,程序不會因為用戶輸入不合法而突然退出,程序會向用戶提示輸入不合法,讓用戶再次輸入。

異常類的繼承體系

當 Python 解釋器接收到異常對象時,如何為該異常對象尋找 except 塊呢?注意上面程序中 except 塊的 except Exception:,這意味著每個 except 塊都是專門用于處理該異常類及其子類的異常實例。

當 Python 解釋器接收到異常對象后,會依次判斷該異常對象是否是 except 塊后的異常類或其子類的實例,如果是,Python 解釋器將調用該 except 塊來處理該異常;否則,再次拿該異常對象和下一個 except 塊里的異常類進行比較。

Python 異常捕獲流程示意圖如圖 1 所示:

Python 異常捕獲流程示意圖
圖 1 Python 異常捕獲流程示意圖

從圖 1 中可以看出,在 try 塊后可以有多個 except 塊,這是為了針對不同的異常類提供不同的異常處理方式。當程序發生不同的意外情況時,系統會生成不同的異常對象,Python 解釋器就會根據該異常對象所屬的異常類來決定使用哪個 except 塊來處理該異常。

通過在 try 塊后提供多個 except 塊可以無須在異常處理塊中使用 if 判斷異常類型,但依然可以針對不同的異常類型提供相應的處理邏輯,從而提供更細致、更有條理的異常處理邏輯。

從圖 1 中可以看出,在通常情況下,如果 try 塊被執行一次,則 try 塊后只有一個 except 塊會被執行,不可能有多個 except 塊被執行。除非在循環中使用了 continue 開始下一次循環,下一次循環又重新運行了 try 塊,這才可能導致多個 except 塊被執行。

Python 的所有異常類都從 BaseException 派生而來,提供了豐富的異常類,這些異常類之間有嚴格的繼承關系,圖 2 顯示了 Python 的常見異常類之間的繼承關系。

Python 的常見異常類之間的繼承關系
圖 2 Python 的常見異常類之間的繼承關系

從圖 2 中可以看出,Python 的所有異常類的基類是 BaseException,但如果用戶要實現自定義異常,則不應該繼承這個基類,而是應該繼承 Exception 類。

BaseException 的主要子類就是 Exception,不管是系統的異常類,還是用戶自定義的異常類,都應該從 Exception 派生。

下面看幾個簡單的異常捕獲的例子:
import sys
try:
    a = int(sys.argv[1])
    b = int(sys.argv[2])
    c = a / b
    print("您輸入的兩個數相除的結果是:", c )
except IndexError:
    print("索引錯誤:運行程序時輸入的參數個數不夠")
except ValueError:
    print("數值錯誤:程序只能接收整數參數")
except ArithmeticError:
    print("算術錯誤")
except Exception:
    print("未知異常")
上面程序,導入了 sys 模塊,并通過 sys 模塊的 argv 列表來獲取運行 Python 程序時提供的參數。其中 sys.argv[0] 通常代表正在運行的 Python 程序名,sys.argv[1] 代表運行程序所提供的第一個參數,sys.argv[2] 代表運行程序所提供的第二個參數……依此類推。

Python 用 import 例來導入模塊,關于模塊和導入模塊會在后續章節進行詳細講解。

上面程序針對 IndexError、ValueError、ArithmeticError 類型的異常,提供了專門的異常處理邏輯。該程序運行時的異常處理邏輯可能有如下幾種情形:
  • 如果在運行該程序時輸入的參數不夠,將會發生索引錯誤,Python 將調用 IndexError 對應的 except 塊處理該異常。
  • 如果在運行該程序時輸入的參數不是數字,而是字母,將發生數值錯誤,Python 將調用 ValueError 對應的 except 塊處理該異常。
  • 如果在運行該程序時輸入的第二個參數是 0,將發生除 0 異常,Python 將調用 ArithmeticError 對應的 except 塊處理該異常。
  • 如果在程序運行時出現其他異常,該異常對象總是 Exception 類或其子類的實例,Python 將調用 Exception 對應的 except 塊處理該異常。

上面程序中的三種異常,都是非常常見的運行時異常,讀者應該記住這些異常,并掌握在哪些情況下可能出現這些異常。

正如在前面程序中所看到的,程序總是把對應 Exception 類的 except 塊放在最后,這是為什么呢?想一下圖 1 所示的 Python 異常捕獲流程,可能你就會明白,如果把 Exception 類對應的 except 塊排在其他 except 塊的前面,Python 解釋器將直接進入該 except 塊(因為所有的異常對象都是 Exception 或其子類的實例),而排在它后面的 except 塊將永遠也不會獲得執行的機會。

實際上,在進行異常捕獲時不僅應該把 Exception 類對應的 except 塊放在最后,而且所有父類異常的 except 塊都應該排在子類異常的 except 塊的后面( 即:先處理小異常,再處理大異常)。

雖然 Python 語法沒有要求,但在實際編程時一定要記住先捕獲小異常,再捕獲大異常。

多異常捕獲

Python 的一個 except 塊可以捕獲多種類型的異常。

在使用一個 except 塊捕獲多種類型的異常時,只要將多個異常類用圓括號括起來,中間用逗號隔開即可,其實就是構建多個異常類的元組。

下面程序示范了 Python 的多異常捕獲:
import sys
try:
    a = int(sys.argv[1])
    b = int(sys.argv[2])
    c = a / b
    print("您輸入的兩個數相除的結果是:", c )
except (IndexError, ValueError, ArithmeticError):
    print("程序發生了數組越界、數字格式異常、算術異常之一")
except:
    print("未知異常")
上面程序中第 7 行代碼使用了(IndexError, ValueError, ArithmeticError)來指定所捕獲的異常類型,這就表明該 except 塊可以同時捕獲這三種類型的異常。

看上面程序中第 9 行代碼,只有 except 關鍵字,并未指定具體要捕獲的異常類型,這種省略異常類的 except 語句也是合法的,它表示可捕獲所有類型的異常,一般會作為異常捕獲的最后一個 except 塊。

訪問異常信息

如果程序需要在 except 塊中訪問異常對象的相關信息,則可通過為異常對象聲明變量來實現。

當 Python 解釋器決定調用某個 except 塊來處理該異常對象時,會將異常對象賦值給 except 塊后的異常變量,程序即可通過該變量來獲得異常對象的相關信息。

所有的異常對象都包含了如下幾個常用屬性和方法:
  • args:該屬性返回異常的錯誤編號和描述字符串。
  • errno:該屬性返回異常的錯誤編號。
  • strerror:該屬性返回異常的描述宇符串。
  • with_traceback():通過該方法可處理異常的傳播軌跡信息。

下面例子演示了程序如何訪問異常信息:
def foo():
    try:
        fis = open("a.txt");
    except Exception as e:
        # 訪問異常的錯誤編號和詳細信息
        print(e.args)
        # 訪問異常的錯誤編號
        print(e.errno)
        # 訪問異常的詳細信息
        print(e.strerror)
foo()
從上面程序可以看出,如果要訪問異常對象,只要在單個異常類或異常類元組(多異常捕獲)之后使用 as 再加上異常變量即可。

在 Python 的早期版本中,直接在單個異常類或異常類元組(多異常捕獲)之后添加異常變量,中間用逗號隔開即可。

上面程序調用了 Exception 對象的 args 屬性(該屬性相當于同時返回 errno 屬性和 strerror 屬性)訪問異常的錯誤編號和詳細信息。運行上面程序,會看到如下運行結果:

(2, 'No such file or directory')
2
No such file or directory

從上面的運行結果可以看到,由于程序嘗試打開的文件不存在,因此引發的異常錯誤編號為 2,異常詳細信息為:No such file or directory。

關于如何處理異常的傳播軌跡信息,后續章節還有更詳細的介紹,此處暫不詳細講解。

上面程序中使用 open() 方法來打開一個文件,用于讀取磁盤文件的內容。關于該 open() 方法后續章節會做詳細介紹。

else 塊

在 Python 的異常處理流程中還可添加一個 else 塊,當 try 塊沒有出現異常時,程序會執行 else 塊。例如如下程序:
s = input('請輸入除數:')
try:
    result = 20 / int(s)
    print('20除以%s的結果是: %g' % (s , result))
except ValueError:
    print('值錯誤,您必須輸入數值')
except ArithmeticError:
    print('算術錯誤,您不能輸入0')
else:
    print('沒有出現異常')
上面程序為異常處理流程添加了 else 塊,當程序中的 try 塊沒有出現異常時,程序就會執行 else 塊。運行上面程序,如果用戶輸入導致程序中的 try 塊出現了異常,則運行結果如下:

請輸入除數:a
值錯誤,您必須輸入數值

如果用戶輸入讓程序中的 try 塊順利完成,則運行結果如下:

請輸入除數:3
20 除以3 的結果是:6.66667
沒有出現異常

看到這里,可能有讀者覺得奇怪:既然只有當 try 塊沒有異常時才會執行 else 塊,那么直接把 else 塊的代碼放在 try 塊的代碼的后面不就行了?

實際上大部分語言的異常處理都沒有 else 塊,它們確實是將 else 塊的代碼直接放在 try 塊的代碼的后面的,因為對于大部分場景而言,直接將 else 塊的代碼放在 try 塊的代碼的后面即可。

但 Python 的異常處理使用 else 塊絕不是多余的語法,當 try 塊沒有異常,而 else 塊有異常時,就能體現出 else 塊的作用了。例如如下程序:
def else_test():
    s = input('請輸入除數:')
    result = 20 / int(s)
    print('20除以%s的結果是: %g' % (s , result))
def right_main():
    try:
        print('try塊的代碼,沒有異常')
    except:
        print('程序出現異常')
    else:
        # 將else_test放在else塊中
        else_test()
def wrong_main():
    try:
        print('try塊的代碼,沒有異常')
        # 將else_test放在try塊代碼的后面
        else_test()
    except:
        print('程序出現異常')
wrong_main()
right_main()
上面程序中定義了一個 else_test() 函數,該函數在運行時需要接收用戶輸入的參數,隨著用戶輸入數據的不同可能導致異常。接下來程序定義了 right_main() 和 wrong_main() 兩個函數,其中 right_main() 將 else_test() 函數放在 else 塊內;而 wrong_main() 將 else_test() 函數放在 try 塊的代碼的后面。

正如上面所介紹的,當 try 塊和 else 塊都沒有異常時,將 else_test() 函數放在 try 塊的代碼的后面和放在 else 塊中沒有任何區別。例如,如果用戶輸入的數據沒有導致程序出現異常,則將看到程序產生如下輸出結果:

try塊的代碼,沒有異常
請輸入除數:4
20除以4的結果是: 5
try塊的代碼,沒有異常
請輸入除數:4
20除以4的結果是: 5

但如果用戶輸入的數據讓 else_test() 函數出現異常(try 塊依然沒有任何異常),此時程序就會產生如下輸出結果:

try塊的代碼,沒有異常
請輸入除數:0
程序出現異常
try塊的代碼,沒有異常
請輸入除數:0
Traceback (most recent call last):
  File "C:\Users\mengma\Desktop\1.py", line 21, in <module>
    right_main()
  File "C:\Users\mengma\Desktop\1.py", line 12, in right_main
    else_test()
  File "C:\Users\mengma\Desktop\1.py", line 3, in else_test
    result = 20 / int(s)
ZeroDivisionError: division by zero

對比上面兩個輸出結果,用戶輸入的都是 0,這樣都會導致 else_test() 函數出現異常。如果將 else_test() 函數放在 try 塊的代碼的后面,此時 else_test() 函數運行產生的異常將會被 try 塊對應的 except 捕獲,這正是 Python 異常處理機制的執行流程:但如果將 else_test() 函數放在 else 塊中,當 else_test() 函數出現異常時,程序沒有 except 塊來處理該異常,該異常將會傳播給 Python 解釋器,導致程序中止。

對比上面兩個輸出結果,不難發現,放在 else 塊中的代碼所引發的異常不會被 except 塊捕獲。

所以,如果希望某段代碼的異常能被后面的 except 塊捕獲,那么就應該將這段代碼放在 try 塊的代碼之后;如果希望某段代碼的異常能向外傳播(不被 except 塊捕獲),那么就應該將這段代碼放在 else 塊中。

精美而實用的網站,提供C語言C++STLLinuxShellJavaGo語言等教程,以及socketGCCviSwing設計模式JSP等專題。

Copyright ?2011-2018 biancheng.net, 陜ICP備15000209號

底部Logo