讀懂Python的異常機制

2021-03-10 10:00:25

前言:之前工作時用python完成一個利用串列埠發SCPI與微控制器互動通訊的命令列視窗,在實現功能的時候發現用python對資料結果無論是最終正確值的返回還是錯誤值的返回都可以直接return給主介面。顯然直接return不同含義的資料是不行的,所以採用異常機制來處理錯誤值的資料。因為之前對異常這方面瞭解的比較少,在此查了點資料並整理個小筆記。

文章目錄

  • 一、對異常的理解
    • 1、什麼是異常
    • 2、錯誤和異常的區別
    • 3、常見python異常種類
  • 二、python五大例外處理機制
    • 1、預設例外處理機制
    • 2、try....except....處理機制
    • 3、try...except...finally.....處理機制
    • 4、assert斷言處理機制
    • 5、with...as處理機制
  • 三、python異常自定義
    • 1、異常自定義
    • 2、異常丟擲raise
    • 3、異常捕獲
  • 四、異常使用注意事項
    • 1、不要太依賴異常機制
    • 2、不要在 try 塊中引入太多的程式碼
    • 3、不要忽略捕獲到的異常
  • 總結

(免費學習推薦:

一、對異常的理解

1、什麼是異常

  異常即「與正常情況不同」,何為正常?正常便是直譯器在解釋程式碼時,我們所編寫的程式碼符合直譯器定義的規則,即為正常,當直譯器發現某段程式碼符合語法但有可能出現不正常的情況時,直譯器便會發出一個事件,中斷程式的正常執行。這個中斷的訊號便是一個異常訊號。所以,總體解釋就是,在直譯器發現到程式出現錯誤的時候,則會產生一個異常,若程式沒有處理,則會將該異常丟擲,程式的執行也隨之終止。我們可以在一個空白的.py檔案中寫一句int(「m」),執行後結果如下。
在這裡插入圖片描述

  這一串字型為直譯器丟擲的一系列錯誤資訊,因為int()傳入的引數只支援數位字串和數位,顯然‘m’不屬於數位字串傳入引數錯誤所以直譯器報「valueError」的錯誤。

2、錯誤和異常的區別

  對於python錯誤的概述:它指的是程式碼執行前的語法或邏輯錯誤。拿常規語法錯誤來說,當我們編寫的程式碼過不了語法檢測時,則會直接出現語法錯誤,必須在程式執行前就改正,不然寫的程式碼將毫無意義,程式碼是不執行的,也無法捕獲得到。舉個例子,在.py檔案輸入if a = 1 print(「hello」),輸出結果如下:

  Traceback (most recent call last):
  	File "E:/Test_code/test.py",line 1
    	if a = 1 print("hello")
                ^SyntaxError: invalid syntax

  函數 print() 被檢查到有錯誤,是它前面缺少了一個冒號 : ,所以解析器會復現句法錯誤的那行程式碼,並用一個小「箭頭」指向行裡檢測到的第一個錯誤,所以我們可以直接找到對應的位置修改其語法。當然除了語法錯誤,還有很多程式奔潰的錯誤,如記憶體溢位等,這類錯誤往往比較隱蔽。
  相比於錯誤,python異常主要在程式執行過程中,程式遇見邏輯或演演算法問題,這時直譯器如果可以處理,則沒問題,如果處理不了,便直接終止程式,便將異常丟擲,如第1小點的int(‘m’)例子,因為引數傳入錯誤導致程式出錯。這種因為邏輯產生的異常五花八門,還好我們的直譯器都內建好了各種異常的種類,讓我們知道是什麼樣的異常出現,好讓我們「對症下藥」。
  這裡注意一點,上述語法錯誤是可識別的錯誤,所以直譯器也會預設丟擲一個SyntaxError異常資訊反饋給程式設計師。所以本質上大部分錯誤都是可被輸出列印的,只是因為錯誤程式碼不執行,也就沒法處理,所以捕獲錯誤的異常資訊就變得沒意義。

3、常見python異常種類

  這裡貼上我們在寫程式碼時最常見的異常型別,如果遇到其他種類的異常,當然是選擇白度啦~

異常名稱名稱解析
BaseException所有異常的基礎類別
SystemExit直譯器請求退出
KeyboardInterrupt使用者中斷執行(通常是輸入^C)
Exception常規錯誤的基礎類別
StopIteration迭代器沒有更多的值
GeneratorExit生成器(generator)發生異常來通知退出
StandardError所有的內建標準異常的基礎類別
ArithmeticError所有數值計算錯誤的基礎類別
FloatingPointError浮點計算錯誤
OverflowError數值運算超出最大限制
ZeropisionError除(或取模)零 (所有資料型別)
AssertionError斷言語句失敗
AttributeError物件沒有這個屬性
EOFError沒有內建輸入,到達EOF 標記
EnvironmentError作業系統錯誤的基礎類別
IOError輸入/輸出操作失敗
OSError作業系統錯誤
WindowsError系統呼叫失敗
ImportError匯入模組/物件失敗
LookupError無效資料查詢的基礎類別
IndexError序列中沒有此索引(index)
KeyError對映中沒有這個鍵
MemoryError記憶體溢位錯誤(對於Python 直譯器不是致命的)
NameError未宣告/初始化物件 (沒有屬性)
UnboundLocalError存取未初始化的本地變數
ReferenceError弱參照(Weak reference)試圖存取已經垃圾回收了的物件
RuntimeError一般的執行時錯誤
NotImplementedError尚未實現的方法
SyntaxError Python語法錯誤
IndentationError縮排錯誤
TabError Tab和空格混用
SystemError一般的直譯器系統錯誤
TypeError對型別無效的操作
ValueError傳入無效的引數
UnicodeError Unicode相關的錯誤
UnicodeDecodeError Unicode解碼時的錯誤
UnicodeEncodeError Unicode編碼時錯誤
UnicodeTranslateError Unicode轉換時錯誤
Warning警告的基礎類別
DeprecationWarning關於被棄用的特徵的警告
FutureWarning關於構造將來語意會有改變的警告
OverflowWarning舊的關於自動提升為長整型(long)的警告
PendingDeprecationWarning關於特性將會被廢棄的警告
RuntimeWarning可疑的執行時行為(runtime behavior)的警告
SyntaxWarning可疑的語法的警告
UserWarning使用者程式碼生成的警告

二、python五大例外處理機制

  我們明白了什麼是異常後,那麼發現異常後怎麼處理,便是我們接下來要解決的問題。這裡將處理異常的方式總結為五種。

1、預設例外處理機制

  「預設」則說明是直譯器預設做出的行為,如果直譯器發現異常,並且我們沒有對異常進行任何預防,那麼程式在執行過程中就會中斷程式,呼叫python預設的例外處理器,並在終端輸出異常資訊。剛才舉過的例子:int(「m」),便是直譯器因為發現引數傳入異常,這種異常直譯器「無能為力」,所以它最後中斷了程式,並將錯誤資訊列印輸出,告訴碼農朋友們:你的程式有bug!!!

2、try…except…處理機制

  我們把可能發生錯誤的語句放在try語句裡,用except來處理異常。每一個try,都必須至少有一個或者多個except。舉一個最簡單的例子如下,在try存取number的第500個元素,很明顯陣列越界存取不了,這時候直譯器會發出異常訊號:IndexError,接著尋找後面是否有對應的異常捕獲語句except ,如果有則執行對應的except語句,待except語句執行完畢後,程式將繼續往下執行。如果沒有對應的except語句,即使用者沒有處理對應的異常,這時直譯器會直接中斷程式並將錯誤資訊列印輸出

number = 'hello'try:	print(number[500])	#陣列越界存取except IndexError:	print("下標越界啦!")except NameError:	print("未宣告物件!")print("繼續執行...")

輸出結果如下,因為直譯器發出異常訊號是IndexError,所以執行下標越界語句。

下標越界啦!
繼續執行...

  為了解鎖更多用法,我們再將例子改一下,我們依然在try存取number的第500個元素,造成存取越界錯誤,這裡的except用了as關鍵字可以獲得異常物件,這樣子便可獲得錯誤的屬性值來輸出資訊。

number = 'hello'try:	print(number[500])	#陣列越界存取except IndexError as e:	print(e)except Exception as e:	#萬能異常
	print(e)except:			  	 #預設處理所有異常
	print("所有異常都可處理")print("繼續執行...")

輸出結果如下所示,會輸出系統自帶的提示錯誤:string index out of range,相對於直譯器因為異常自己丟擲來的一堆紅色刺眼的字型,這種看起來舒服多了(能夠「運籌帷幄」的異常才是好異常嘛哈哈哈)。另外這裡用到「萬能異常」Exception,基本所有沒處理的異常都可以在此執行。最後一個except表示,如果沒有指定異常,則預設處理所有的異常。

string index out of range繼續執行...

3、try…except…finally…處理機制

  finally語句塊表示,無論異常發生與否,finally中的語句都要執行完畢。也就是可以很霸氣的說,無論產生的異常是被except捕獲到處理了,還是沒被捕獲到直譯器將錯誤輸出來了,都統統要執行這個finally。還是原來簡單的例子加上finally語句塊如下,程式碼如下:

number = 'hello'try:	print(number[500])	#陣列越界存取,丟擲IndexError異常except IndexError:	print("下標越界啦!")finally:	print("finally!")print("繼續執行...")		#執行

結果如下,資料越界存取異常被捕獲到後,先執行except 語句塊,完畢後接著執行了finally語句塊。因為異常被執行,所以後面程式碼繼續執行。

下標越界啦!finally!
繼續執行...

  對try語句塊進行修改,列印abc變數值,因為abc變數沒定義,所以會出現不會被捕獲的NameError異常訊號,程式碼如下所示:

number = 'hello'try:	print(abc)	#變數未被定義,丟擲NameError異常except IndexError:	print("下標越界啦!")finally:	print("finally!")print("繼續執行...")	#不執行

結果如下,因為NameError異常訊號沒法被處理,所以直譯器將程式中斷,並將錯誤資訊輸出,但這過程中依然會執行finally語句塊的內容。因為程式被迫中斷了,所以後面程式碼不執行。

finally!	#異常沒被捕獲,也執行了finallyTraceback (most recent call last):
	File "E:/Test_code/test.py",line 3,in <module>
   		print("abc")NameError: name 'abc' is not defined

  理解到這裡,相信:try…finally…這種機制應該也不難理解了,因為省略了except 捕獲異常機制,所以異常不可能被處理,直譯器會將程式中斷,並將錯誤資訊輸出,但finally語句塊的內容依然會被執行。例子程式碼如下:

number = 'hello'try:	print(abc)	#變數未被定義,丟擲NameError異常finally:	print("finally!")print("繼續執行...")

執行結果:

finally!	#異常沒被捕獲,也執行了finallyTraceback (most recent call last):
	File "E:/Test_code/test.py",line 3,in <module>
   		print("abc")NameError: name 'abc' is not defined

4、assert斷言處理機制

  assert語句先判斷assert後面緊跟的語句是True還是False,如果是True則繼續往下執行語句,如果是False則中斷程式,將錯誤資訊輸出。

assert 1 == 1 	#為True正常執行assert 1 == 2	#為False,終止程式,錯誤資訊輸出

5、with…as處理機制

  with…as一般常用在檔案處理上,我們平時在使用類似檔案的流物件時,使用完畢後要呼叫close方法關閉,很麻煩,這裡with…as語句提供了一個非常方便且人性的替代方法,即使突發情況也能正常關閉檔案。舉個例子程式碼如下,open開啟檔案後將返回的檔案流物件賦值給fd,然後在with語句塊中使用。

with open('e:/test.txt','r') as fd:
	fd.read()
	print(abc)	#變數未被定義,程式終止,錯誤資訊輸出print("繼續執行...")

  正常情況下,這裡的with語句塊完畢之後,會自動關閉檔案。但如果with語句執行中發生異常,如程式碼中的變數未定義異常,則會採用預設例外處理機制,程式終止,錯誤資訊輸出,後面程式碼不被執行,檔案也會正常關閉。

三、python異常自定義

  說了這麼多異常的使用,終於可以回到我前言所說的在實際專案中存在的問題,即錯誤碼的返回和數值的返回是衝突的(因為錯誤碼也是數值),這時候便可以用異常的丟擲和捕獲來完成錯誤碼的傳遞,即try和except 。但系統發生異常時丟擲的是系統本身定義好的異常型別,跟自己的錯誤碼又有何關係?這就是我接下來要說的內容:如何定義自己的異常並且能夠被except 所捕獲

1、異常自定義

  實際開發中,有時候系統提供的異常型別往往都不能滿足開發的需求。這時候就要使用到異常的自定義啦,你可以通過建立一個新的異常類來擁有自己的異常。自己定義的異常類繼承自 Exception 類,可以直接繼承,或者間接繼承。栗子舉起來:

class MyException(Exception):
    '''自定義的異常類'''
    def __init__(self, error_num):	#異常類物件的初始化屬性
        self.error_num = error_num    def __str__(self):				#返回異常類物件說明資訊
        err_info = ['超時錯誤','接收錯誤']
        return err_info[self.error_num]

  該類繼承自Exception 類,並且新類的名字為MyException,這跟前面我們一直在用的IndexError這個異常類一樣,都是繼承自Exception 類。__init__為建構函式,當我們建立物件時便會自動呼叫,__str__為物件說明資訊函數,當使用print輸出物件的時候,只要自己定義了__str__方法,那麼就會列印從在這個方法中return的資料。
  即print(MyException(0))時,便可列印「超時錯誤」這個字串,print(MyException(1))時,便可列印「接收錯誤」這個字串,心細的你應該可以理解,MyException(x)為臨時物件(x是傳入錯誤碼引數,這裡只定義了0和1),與a = MyException(x),a為物件一個樣子 。 這裡有一個好玩的說法,在python中方法名如果是__xxxx__()的,那麼就有特殊的功能,因此叫做「魔法」方法。

2、異常丟擲raise

  現在我們自己定義的錯誤定義好了(上面的MyException),怎麼能像IndexError一樣讓except捕獲到呢?於是乎raise關鍵字派上用場。我們在異常機制中用try…except時,一般都是將可能產生的錯誤程式碼放到try語句塊中,這時出現異常則系統便會自動將其丟擲,比如IndexError,這樣except就能捕獲到,所以我們只要將自定義的異常在需要的時候將其丟擲即可。
  raise 唯一的一個引數指定了要被丟擲的異常。它必須是一個異常的範例或者是異常的類(也就是 Exception 的子類),那麼我們剛剛定義的異常類就可以用啦,舉個簡單例子:

try:
    raise MyException(0)	# 自己定義的錯誤類,將錯誤碼為0的錯誤丟擲except MyException as e:
    print(e) 	  			# 輸出的是__str__返回的內容,即「超時錯誤」

  這裡我直接將自己定義的錯誤丟擲,…as e就是把得到的錯誤當成物件e,這樣才可以存取其屬性和方法。因為自己定義的錯誤中可以支援多個錯誤碼(本質還是MyException這個錯誤),所以便可實現傳入不同錯誤碼就可列印不同錯誤資訊。

3、異常捕獲

  只要我們在try中將錯誤raise出來,except就可以捕獲到(當然,異常必須是Exception 子類才能被捕獲),將前面兩個例子整合起來,程式碼如下:

'''錯誤碼:0代表超時錯誤,1代表接收錯誤'''class MyException(Exception):
    '''自定義的異常類'''
    def __init__(self, error_num):	# 異常類物件的初始化屬性
        self.error_num= error_num    def __str__(self):				# 返回異常類物件指定錯誤碼的資訊
        err_info = ['超時錯誤','接收錯誤']
        return err_info[self.error_num]def fun()
	raise MyException(1) 			# 丟擲異常物件,傳入錯誤碼1def demo_main():
    try:
        fun()
    except MyException as ex:		# 這裡要使用MyException進行捕獲,物件為ex
        print(ex) 	   				# 輸出的是__str__部分返回的內容,即「接收錯誤」
        print(ex.error_num) 		# 輸出的是__init__中定義的error_num,即1demo_main()							#此處開始執行

  程式碼從demo_main函數開始執行,進入try語句塊,語句塊中的fun()函數模擬程式碼執行失敗時raise 自定義的異常,except 正常接收後通過as 關鍵字得到異常物件,存取該異常物件,便可正常輸出自定義的異常資訊和自定義的錯誤碼。

四、異常使用注意事項

此注意事項參考博文:異常機制使用細則.

1、不要太依賴異常機制

  python 的異常機制非常方便,對於資訊的傳遞中十分好用(這裡資訊的傳遞主要有三種,引數傳遞,全域性變數傳遞,以及異常機制傳遞),但濫用異常機制也會帶來一些負面影響。過度使用異常主要表現在兩個方面:①把異常和普通錯誤混淆在一起,不再編寫任何錯誤處理程式碼,而是以簡單地引發異常來代苦所有的錯誤處理。②使用例外處理來代替流程控制。例子如下:

buf = "hello"#例1:使用例外處理來遍歷arr陣列的每個元素try:
    i = 0    
    while True:
        print (buf [i])
        i += 1except:
    pass#例2:使用流程控制避免下標存取異常i = 0while i < len(buf ):
    print(buf [i])
    i += 1

  例1中假如迴圈過度便會下標存取異常,這時候把錯誤丟擲,再進行一系列處理,顯然是不可取的,因為異常機制的效率比正常的流程控制效率差,顯然例2中簡單的業務流程就可以避開這種錯誤。所以不要熟悉了異常的使用方法後,遇到這種簡單邏輯,便不管三七二十一引發異常後再進行解決。對於完全己知的錯誤和普通的錯誤,應該編寫處理這種錯誤的程式碼,增加程式的健壯性。只有對於外部的、不能確定和預知的執行時錯誤才使用異常

2、不要在 try 塊中引入太多的程式碼

  在 try 塊裡放置大量的程式碼,這看上去很「簡單」,程式碼框架很容易理解,但因為 try 塊裡的程式碼過於龐大,業務過於複雜,就會造成 try 塊中出現異常的可能性大大增加,從而導致分析異常原因的難度也大大增加。
  而且當塊過於龐大時,就難免在 try 塊後緊跟大量的 except 塊才可以針對不同的異常提供不同的處理邏輯。在同一個 try 塊後緊跟大量的 except 塊則需要分析它們之間的邏輯關係,反而增加了程式設計複雜度。所以,可以把大塊的 try 塊分割成多個小塊,然後分別捕獲並處理異常。

3、不要忽略捕獲到的異常

  不要忽略異常!既然己捕獲到異常,那麼 except 塊理應做些有用的事情,及處理並修復異常。except 塊整個為空,或者僅僅列印簡單的異常資訊都是不妥的!具體的處理方式為:
處理異常。對異常進行合適的修復,然後繞過異常發生的地方繼續執行;或者用別的資料進行計算,以代替期望的方法返回值;或者提示使用者重新操作,總之,程式應該儘量修復異常,使程式能恢復執行。
重新引發新異常。把在當前執行環境下能做的事情儘量做完,然後進行異常轉譯,把異常包裝成當前層的異常,重新傳給上層呼叫者。
在合適的層處理異常
。如果當前層不清楚如何處理異常,就不要在當前層使用 except 語句來捕獲該異常,讓上層呼叫者來負責處理該異常。

總結

  本文從系統預設的異常起手,說明了什麼是異常並總結了系統常見的異常類,接著寫了怎麼自定義異常,從異常的定義到丟擲再到獲取完成自定義異常的定義和使用,最後再總結了python異常使用時的注意事項。

相關免費學習推薦:(視訊)

以上就是讀懂Python的異常機制的詳細內容,更多請關注TW511.COM其它相關文章!