1. 程式人生 > 其它 >Python學習筆記:異常處理

Python學習筆記:異常處理

第七種 異常處理

Python的異常機制主要依賴 try、except、else、finally和raise五個關鍵字,其中在try關鍵字後縮排的程式碼塊簡稱try塊,它裡面放置的是可能引發異常的程式碼;在except後對應的是異常型別和一個程式碼塊,用於表明該except塊處理這種異常型別的程式碼塊;在多個except塊之後可以放一個else塊,表明程式不出現異常時還要執行else塊;最後還可以跟一個finally塊,finally塊用於回收在try塊裡開啟的物理資源,異常處理機制會保證finally塊總被執行;而raise用於引發一個實際的異常,raise可以單獨作為語句使用,引發一個具體的異常物件。

異常概述

異常處理機制

異常處理機制可以讓程式具有極好的容錯性、讓程式更加健壯。

使用try...excepy捕獲異常

python的異常處理機制的語法結構:

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

如果在執行try塊裡的業務邏輯程式碼時出現異常,系統自動生成一個異常物件,該異常物件被提交給python直譯器,這個過程被稱為引發異常。
當python直譯器收到異常物件時,會尋找能處理該異常物件的except塊,如果找到合適的except塊,則把該異常物件交給該except塊處理,這個過程被稱為捕獲異常。如果python直譯器找不到捕獲異常的except塊,則執行時環境終止,python直譯器也將退出。

# 定義棋盤的大小
BOARD_SIZE = 15
# 定義一個二維列表來充當棋盤
board = []
def initBoard() :
    # 把每個元素賦為"╋",用於在控制檯畫出棋盤
    for i in range(BOARD_SIZE) :
        row = ["╋"] * BOARD_SIZE
        board.append(row)
# 在控制檯輸出棋盤的方法
def printBoard() :
    # 列印每個列表元素
    for i in range(BOARD_SIZE) :
        for j in range(BOARD_SIZE) :
            # 列印列表元素後不換行
            print(board[i][j], end="")
        # 每列印完一行列表元素後輸出一個換行符
        print()
initBoard()
printBoard()
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
    '''
     電腦隨機生成2個整數,作為電腦下棋的座標,賦給board列表
     還涉及
        1.座標的有效性,只能是數字,不能超出棋盤範圍
        2.下的棋的點,不能重複下棋
        3.每次下棋後,需要掃描誰贏了
    '''
    printBoard()
    inputStr = input("請輸入您下棋的座標,應以x,y的格式:\n")

輸出結果:

╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
請輸入您下棋的座標,應以x,y的格式:
12
您輸入的座標不合法,請重新輸入,下棋座標應以x,y的格式
1,2
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
●╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
請輸入您下棋的座標,應以x,y的格式:

異常類的繼承體系



多個except塊是為了針對不同的異常類提供不用的處理方式。
python所有的異常類都是從BaseException派生而來,提供了豐富的異常類,這些異常類之間有嚴格的繼承關係。

如果使用者要實現自定義異常,則不應該繼承這個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.argv[0]:通常代表正在執行的python程式名
  • sys.argv[1]:代表執行程式所提供的第一個引數
  • sys.argv[2]:代表執行程式所提供的第二個引數...

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

多異常捕獲

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

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("未知異常")
    
輸出結果:
程式發生了陣列越界、數字格式異常、算術異常之一

省略異常類的except也是合法的,它表示可捕獲所有型別的異常,一般會作為異常捕獲的最後一個except塊。

訪問異常資訊

如果需要在except塊中訪問異常物件的相關資訊,則可通過為異常物件宣告變數來實現,只要在單個異常類或異常類元組(多異常捕獲)之後使用as再加上異常變數即可
所有的異常物件都包含了如下幾個常用屬性和方法

  • args:該屬性返回異常的錯誤編號和描述字串(該屬性相當於同時返回errno屬性和strerror屬性)
  • 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()


輸出結果:
(2, 'No such file or directory')
2
No such file or directory

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('沒有出現異常')

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


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

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()

輸出結果:
try塊的程式碼,沒有異常

請輸入除數:0
程式出現異常
try塊的程式碼,沒有異常

請輸入除數:0
Traceback (most recent call last):

  File "C:\Users\zz\.spyder-py3\temp.py", line 21, in <module>
    right_main()

  File "C:\Users\zz\.spyder-py3\temp.py", line 12, in right_main
    else_test()

  File "C:\Users\zz\.spyder-py3\temp.py", line 3, in else_test
    result = 20 / int(s)

ZeroDivisionError: division by zero

放在 else 塊中的代間所引發的異常不會被 except 塊捕獲。所以,如果希望某段程式碼的異常能被後面的 except 塊捕獲,那麼就應該將這段程式碼放在 try 塊的程式碼之後 ; 如果希望某段程式碼的異常能向外傳播(不被 except 塊捕獲〉,那麼就應該將這段程式碼放在else塊中。

使用finally回收資源

有些時候,程式在try塊中打開了一些物理資源(如資料庫連線、網路連線和磁碟檔案等),這些物理資源都必須被顯示回收。
不管try塊中的程式碼是否出現異常,也不管哪一個except塊被執行,甚至在try塊或except塊中執行了return語句,finally塊總會被執行。python完整的異常處理語法結構如下:

try:
    # 業務功能程式碼
    ......
except subException as e:
    # 異常處理塊1
    ......
except subException as e:
    # 異常處理塊2
    ......
else:
    # 正常處理塊
finally:
    # 資源回收塊
    ...

try塊是必須的,如果沒有try塊,則不能有後面的except塊和finally塊。except塊和finally都是可選的,但except塊和finally塊至少出現其中之一,也可以同時出現。

import os
def test():
    fis = None
    try:
        fis = open("a.txt")
    except OSError as e:
        print(e.strerror)
        # return語句強制方法返回
        return        # ①
        # os._exit(1)     # ②
    finally:
        # 關閉磁碟檔案,回收資源
        if fis is not None:
            try:
                # 關閉資源
                fis.close()
            except OSError as ioe:
                print(ioe.strerror)
        print("執行finally塊裡的資源回收!")
test()


輸出結果:
No such file or directory
執行finally塊裡的資源回收!

如果在異常處理處理程式碼中使用os._exit(1)語句來退出python直譯器,則finally塊將會失去執行的機會。
注意:
除非在try塊 、 except塊中呼叫了退出 Python 直譯器的方法,否則不管在 try 塊、except 塊中執行怎樣的程式碼,出現怎樣的情況,異常處理的 finally 塊總會被執行。 呼叫 sys.exit() 方法退出程式不能阻止 finally 塊的執行,這是因為 sys.exit()方法本身就是通過引發 SystemExit 異常來退出程式的。
一旦在finally塊中使用了return或raise語句,將會導致try塊、except塊中的return或raise語句失效,示例:

def test():
    try:
        # 因為finally塊中包含了return語句
        # 所以下面的return語句失去作用
        return True
    finally:
        return False
a = test()
print(a)

輸出結果:
False

如果Python程式在執行try塊、except塊時遇到了return或raise語句,這兩條語句都會導致該方法立即結束,那麼系統執行這兩條語句並不會結束該方法,而是去尋找該異常處理流程中的finally塊,如果沒有找到finally塊,程式立即執行return或raise語句,方法中止;如果找到finally塊,系統立即開始執行finally塊一一隻有當finally塊執行完成後,系統才會再次跳回來執行try塊、except塊裡的return或raise語句:如果在finally塊裡也使用了return或raise等導致方法中止的語句,finally塊己經中止了方法,系統將不會跳回去執行t可塊、except塊裡的任何程式碼。

異常處理巢狀

異常處理流程程式碼可以被放在任何能放可執行程式碼的地方,因此完整的異常處理流程既可被放在try塊裡,也可被放在except塊裡,還可被放在finally塊裡。

使用raise引發異常

python允許程式自發引發異常,自發引發異常使用raise語句來完成

引發異常

異常是一種很“主觀”的說法,與業務需求不符而產生的異常,必須由程式設計師來決定引發,系統無法引發這種異常。
raise語句有三種常用方法:

  • raise:單獨一個raise。該語句引發當前上下文中捕獲的異常(比如在except塊中),或預設引發RuntimeError異常
  • raise 異常類:raise後帶一個異常類,該語句引發指定異常類的預設例項
  • raise 異常物件:引發指定的異常物件

raise語句每次只能引發一個異常例項

import traceback
def main():
    try:
        # 使用try...except來捕捉異常
        # 此時即使程式出現異常,也不會傳播給main函式
        mtd(3)
    except Exception as e:
        print('程式出現異常:', e)
#        help(e.with_traceback)
        traceback.print_exc()
#        e.with_traceback(e)
    # 不使用try...except捕捉異常,異常會傳播出來導致程式中止
    #mtd(3)
def mtd(a):
    if a > 0:
        raise ValueError("a的值大於0,不符合要求")
main()


輸出結果:
程式出現異常: a的值大於0,不符合要求
Traceback (most recent call last):
  File "C:\Users\zhanghu\.spyder-py3\temp.py", line 6, in main
    mtd(3)
  File "C:\Users\zhanghu\.spyder-py3\temp.py", line 16, in mtd
    raise ValueError("a的值大於0,不符合要求")
ValueError: a的值大於0,不符合要求

如果不捕獲異常,會發生什麼?示例:

import traceback
def main():
    mtd(3)
def mtd(a):
    if a > 0:
        raise ValueError("a的值大於0,不符合要求")
main()


輸出結果:
Traceback (most recent call last):

  File "C:\Users\zhanghu\.spyder-py3\temp.py", line 7, in <module>
    main()

  File "C:\Users\zhanghu\.spyder-py3\temp.py", line 3, in main
    mtd(3)

  File "C:\Users\zhanghu\.spyder-py3\temp.py", line 6, in mtd
    raise ValueError("a的值大於0,不符合要求")

ValueError: a的值大於0,不符合要求

程式既可在呼叫mtd(3)時使用try...except來捕獲異常,這樣該異常將會被except塊捕獲,不會傳播給呼叫它的函式;也可直接呼叫mtd(3),這樣該函式的異常就會直接傳播給它的呼叫函式,如果該函式也不處理該異常,就會導致程式中止。

自定義異常類

使用者自定義異常都應該繼承Exception基類或Exception的子類,在自定義異常類時基本不需要書寫更多的程式碼,只需要指定自定義異常類的父類即可。

class AuctionException(Exception): pass

該異常類不需要類體定義,使用pass語句作為佔位符即可

except和raise同時使用

在實際應用中對異常可能需要更復雜的處理方式一一當一個異常出現時,單靠某個方法無法完全處理該異常,必須由幾個方法協作才可完全處理該異常。也就是說,在異常出現的當前方法中,程式只對異常進行部分處理,還有些處理需要在該方法的呼叫者中才能完成,所以應該再次引發異常,讓該方法的呼叫者也能捕獲到異常。
為了實現這種通過多個方法協作處理同一個異常的情形,可以在except塊中結合raise語句來完成。

class AuctionException(Exception): pass
class AuctionTest:
    def __init__(self, init_price):
        self.init_price = init_price
    def bid(self, bid_price):
        d = 0.0
        try:
            d = float(bid_price)
        except Exception as e:
            # 此處只是簡單地列印異常資訊
            print("轉換出異常:", e)
            # 再次引發自定義異常
#            raise AuctionException("競拍價必須是數值,不能包含其他字元!")  # ①
            raise AuctionException(e)
        if self.init_price > d:
            raise AuctionException("競拍價比起拍價低,不允許競拍!")
        initPrice = d
def main():
    at = AuctionTest(20.4)
    try:
        at.bid("df")
    except AuctionException as ae:
        # 再次捕獲到bid()方法中的異常,並對該異常進行處理
        print('main函式捕捉的異常:', ae)
main()


輸出結果:
轉換出異常: could not convert string to float: 'df'
main函式捕捉的異常: could not convert string to float: 'df'

這種except和raise結合使用的情況在實際應用中非常常用。實際應用對異常的處理通常分成兩個部分:①應用後臺需要通過日誌來記錄異常發生的詳細情況;②應用還需要根據異常向應用使用者傳達某種提示。在這種情形下,所有異常都需要兩個方法共同完成,也就必須將except和raise結合使用。
如果程式需要將原始異常的詳細資訊直接傳播出去,Python也允許用自定義異常對原始異常進行包裝,只要將上面①號粗體字程式碼改為如下形式。

raise AuctionException(e)

上面就是把原始異常e包裝成了AuctionException異常,這種方式也被稱為異常包裝或異常轉譯

raise不需要引數

在使用raise語句時可以不帶引數,此時raise語句處於except塊中,它將會自動引發當前上下文啟用的異常;否則,預設引發RuntimeError異常

class AuctionException(Exception): pass
class AuctionTest:
    def __init__(self, init_price):
        self.init_price = init_price
    def bid(self, bid_price):
        d = 0.0
        try:
            d = float(bid_price)
        except Exception as e:
            # 此處只是簡單地列印異常資訊
            print("轉換出異常:", e)
            # 再次引發自定義異常
#            raise AuctionException("競拍價必須是數值,不能包含其他字元!")  # ①
            raise
        if self.init_price > d:
            raise AuctionException("競拍價比起拍價低,不允許競拍!")
        initPrice = d
def main():
    at = AuctionTest(20.4)
    try:
        at.bid("df")
    except Exception as ae:
        # 再次捕獲到bid()方法中的異常,並對該異常進行處理
        print('main函式捕捉的異常:', ae)
main()


輸出結果:
轉換出異常: could not convert string to float: 'df'
main函式捕捉的異常: could not convert string to float: 'df'

此時main()函式再次捕獲了ValueError --- 它就是在bid()方法中except塊所捕獲的原始異常。

Python的異常傳播軌跡

異常物件提供了一個with_traceback用於處理異常的傳播軌跡,檢視異常的傳播軌跡可追蹤異常觸發的源頭,也可看到異常一路觸發的軌跡。

class SelfException(Exception): pass

def main():
    firstMethod()
def firstMethod():
    secondMethod()
def secondMethod():
    thirdMethod()
def thirdMethod():
    raise SelfException("自定義異常資訊")
main()


輸出結果:
Traceback (most recent call last):

  File "C:\Users\zz\.spyder-py3\temp.py", line 11, in <module>
    main()

  File "C:\Users\zz\.spyder-py3\temp.py", line 4, in main
    firstMethod()

  File "C:\Users\zz\.spyder-py3\temp.py", line 6, in firstMethod
    secondMethod()

  File "C:\Users\zz\.spyder-py3\temp.py", line 8, in secondMethod
    thirdMethod()

  File "C:\Users\zz\.spyder-py3\temp.py", line 10, in thirdMethod
    raise SelfException("自定義異常資訊")

SelfException: 自定義異常資訊

異常從thirdMethod()函式開始觸發,傳到secondMethod()函式,再傳到firstMethod()函式,最後傳到main()函式,在main()函式止,這個過程就是Python的異常傳播軌跡。
在實際應用程式的開發中,大多數複雜操作都會被分解成一系列函式或方法呼叫。這是因為:為了具有更好的可重用性,會將每個可重用的程式碼單元定義成函式或方法,將複雜任務逐漸分解為更易管理的小型子任務。由於一個大的業務功能需要由多個函式或方法來共同實現,在最終程式設計模型中,很多物件將通過一系列函式或方法呼叫來實現通訊,執行任務。所以,當應用程式執行時,經常會發生一系列函式或方法呼叫,從而形成“函式呼叫棧”。異常的傳播則相反:只要異常沒有被完全捕獲(包括異常沒有被捕獲,或者異常被處理後重新引發了新異常),異常就從發生異常的函式或方法逐漸向外傳播,首先傳給該函式或方法的呼叫者,該函式或方法的呼叫者再傳給其呼叫者……直至最後傳到Python直譯器,此時Python直譯器會中止該程式,並列印異常的傳播軌跡資訊。
python專門提供了traceback模組來處理異常傳播軌跡,使用traceback可以方便地處理python的異常傳播軌跡,traceback提供了兩個常用方法:

  • traceback.print_exc():將異常傳播軌跡資訊輸出到控制檯或指定檔案中
  • format_exc():將異常傳播軌跡資訊轉換成字串。
# 匯入trackback模組
import traceback
class SelfException(Exception): pass

def main():
    firstMethod()
def firstMethod():
    secondMethod()
def secondMethod():
    thirdMethod()
def thirdMethod():
    raise SelfException("自定義異常資訊")
try:
    main()
except:
    # 捕捉異常,並將異常傳播資訊輸出控制檯
    traceback.print_exc()
    # 捕捉異常,並將異常傳播資訊輸出指定檔案中
    traceback.print_exc(file=open('log.txt', 'a'))
    
    
輸出結果:
Traceback (most recent call last):
  File "C:\Users\zz\.spyder-py3\temp.py", line 14, in <module>
    main()
  File "C:\Users\zz\.spyder-py3\temp.py", line 6, in main
    firstMethod()
  File "C:\Users\zz\.spyder-py3\temp.py", line 8, in firstMethod
    secondMethod()
  File "C:\Users\zz\.spyder-py3\temp.py", line 10, in secondMethod
    thirdMethod()
  File "C:\Users\zz\.spyder-py3\temp.py", line 12, in thirdMethod
    raise SelfException("自定義異常資訊")
SelfException: 自定義異常資訊

異常處理原則

成功的異常處理應該實現如下4個目標。

  • 使程式程式碼混亂最小化。
  • 捕獲並保留診斷資訊。
  • 通知合適的人員。
  • 採用合適的方式結束異常活動。

不要過度使用異常

必須指出:異常處理機制的初衷是將不可預期異常的處理程式碼和正常的業務邏輯處理程式碼分離,因此絕不要使用異常處理來代替正常的業務邏輯判斷。

不是使用過於龐大的try塊

正確的做法是,把大塊的try塊分割成多個可能出現異常的程式段落,並把它們放在單獨的try塊中,從而分別捕獲並處理異常。

不要忽略捕獲到的異常

通常建議對異常採取適當措施,比如:

  • 處理異常。對異常進行合適的修復,然後繞過異常發生的地方繼續執行;或者用別的資料進行計算,以代替期望的方法返回值;或者提示使用者重新操作……總之,程式應該儘量修復異常,使程式能恢復執行。
  • 重新引發新異常。把在當前執行環境下能做的事情儘量做完,然後進行異常轉譯,把異常包裝成當前層的異常,重新傳給上層呼叫者。
  • 在合適的層處理異常。如果當前層不清楚如何處理異常,就不要在當前層使用except語句來捕獲該異常,讓上層呼叫者來負責處理該異常。

原文來源於我的語雀,我的微信公眾號:細細研磨