Python 異常處理
一,異常處理
在程序運行的過程中,如果發生了錯誤,可以事先約定返回一個錯誤代碼,這樣,就可以知道是否有錯,以及出錯的原因。
高級語言通常都內置了一套try...except...finally...
的錯誤處理機制,Python也不例外
import traceback
def calc(a,b):
res = a/b
return res
def main():
money = input(‘輸入多少錢:‘)
months = input(‘還幾個月:‘)
try:
res = calc(int(money),int(months))
except ZeroDivisionError as e: #try裏面的代碼如果出錯了,走except裏面的代碼
traceback.print_exc()#只是輸出報錯的詳細信息而已
print(‘還款的月數不能小於1‘,e)
except ValueError as e:
print(‘輸入必須是整數,%s‘%e)
except Exception as e: #捕獲所有的異常
print(‘未知錯誤!%s‘%e)
else:#沒有出錯的情況下走else
print(‘每個月應該還%s‘%res)
finally:#
print(‘finally..‘)
print(‘END‘)
main()
輸出結果:
輸入多少錢:500
還幾個月:0
還款的月數不能小於1 division by zero
finally..
END
輸出報錯的詳細信息:traceback.print_exc()
Traceback (most recent call last):
File "C:/Users/lidal/PycharmProjects/llq-code/day7/練習.py", line 9, in main
res = calc(int(money),int(months))
File "C:/Users/lidal/PycharmProjects/llq-code/day7/練習.py", line 3, in calc
res = a/b
ZeroDivisionError: division by zero
當我們認為某些代碼可能會出錯時,就可以用try
來運行這段代碼,如果執行出錯,則後續代碼不會繼續執行,
而是直接跳轉至錯誤處理代碼,即except
語句塊,執行完except
後,如果有finally
語句塊,則執行finally
語句塊,至此,執行完畢。
如果沒有錯誤發生,except
語句塊不會被執行,但是finally
如果有,則一定會被執行(可以沒有finally
語句)。
二,常見的異常
AttributeError: 試圖訪問一個對象沒有的屬性,比如foo.x,但是foo沒有屬性x
IOError:輸入 / 輸出異常,一般是無法打開文件
ImportError: 無法導入模塊或包,一般是路徑問題或名稱錯誤
IndentationError:代碼沒有正確對齊,屬於語法錯誤
IndexError:下標索引超出序列邊界,比如x只有三個元素,卻試圖訪問x[3]
KeyError:試圖訪問字典裏不存在的鍵
KeyboardInterrupt:Ctrl + C被按下
NameError:使用一個還未被賦予對象的變量
SyntaxError: 語法錯誤
TypeError: 傳入對象類型與要求的不符
UnboundLocalError:試圖訪問一個還未被設置的局部變量,一般是由於在代碼塊外部還有另一個同名變量
ValueError: 傳入一個調用者不期望的值,即使值的類型是正確的
Python所有的錯誤都是從BaseException
類派生的,常見的錯誤類型和繼承關系看這裏:
https://docs.python.org/3/library/exceptions.html#exception-hierarchy
三,調用棧
如果錯誤沒有被捕獲,它就會一直往上拋,最後被Python解釋器捕獲,打印一個錯誤信息,然後程序退出
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
bar(‘0‘)
main()
輸出結果:
Traceback (most recent call last):#告訴我們這是錯誤的跟蹤信息
File "C:/Users/lidal/PycharmProjects/llq-code/day7/練習.py", line 10, in <module>
main() #調用main()出錯了,在代碼文件err.py的第10行代碼,但原因是第8行:
File "C:/Users/lidal/PycharmProjects/llq-code/day7/練習.py", line 8, in main
bar(‘0‘) #調用bar(‘0‘)出錯了,在代碼文件err.py的第8行代碼,但原因是第5行:
File "C:/Users/lidal/PycharmProjects/llq-code/day7/練習.py", line 5, in bar
return foo(s) * 2 #原因是return foo(s) * 2這個語句出錯了,但這還不是最終原因,繼續往下看:
File "C:/Users/lidal/PycharmProjects/llq-code/day7/練習.py", line 2, in foo
return 10 / int(s)#原因是return 10 / int(s)這個語句出錯了,這是錯誤產生的源頭,因為下面打印了:
ZeroDivisionError: division by zero
#根據錯誤類型ZeroDivisionError,我們判斷,int(s)本身並沒有出錯,但是int(s)返回0,
# 在計算10 / 0時出錯,至此,找到錯誤源頭。
四,記錄錯誤
如果不捕獲錯誤,自然可以讓Python解釋器來打印出錯誤堆棧,但程序也被結束了。既然我們能捕獲錯誤,
就可以把錯誤堆棧打印出來,然後分析錯誤原因,同時,讓程序繼續執行下去。
Python內置的logging
模塊可以非常容易地記錄錯誤信息:
import logging
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
try:
bar(‘0‘)
except Exception as e:
logging.exception(e)
main()
print(‘END‘)
同樣是出錯,但程序打印完錯誤信息後會繼續執行,並正常退出:
END
ERROR:root:division by zero
Traceback (most recent call last):
File "C:/Users/lidal/PycharmProjects/llq-code/day7/練習.py", line 11, in main
bar(‘0‘)
File "C:/Users/lidal/PycharmProjects/llq-code/day7/練習.py", line 7, in bar
return foo(s) * 2
File "C:/Users/lidal/PycharmProjects/llq-code/day7/練習.py", line 4, in foo
return 10 / int(s)
ZeroDivisionError: division by zero
通過配置,logging
還可以把錯誤記錄到日誌文件裏,方便事後排查。
五,拋出錯誤
主動拋出異常,就是我們在代碼裏面讓它自動拋出一個異常,然後捕捉到,比如說我們在寫自動化測試腳本的時候,
結果和預期不符合,就可以主動拋出一個異常信息,然後捕捉到,做其他的處理,主動拋出異常使用raise關鍵字。
因為錯誤是class,捕獲一個錯誤就是捕獲到該class的一個實例。因此,錯誤並不是憑空產生的,而是有意創建並拋出的。Python的內置函數會拋出很多類型的錯誤,我們自己編寫的函數也可以拋出錯誤。
如果要拋出錯誤,首先根據需要,可以定義一個錯誤的class,選擇好繼承關系,然後,用raise
語句拋出一個錯誤的實例:
class FooError(ValueError):
pass
def foo(s):
n = int(s)
if n==0:
raise FooError(‘invalid value: %s‘ % s)
return 10 / n
foo(‘0‘)
執行,可以最後跟蹤到我們自己定義的錯誤:
Traceback (most recent call last):
File "C:/Users/lidal/PycharmProjects/llq-code/day7/練習.py", line 10, in <module>
foo(‘0‘)
File "C:/Users/lidal/PycharmProjects/llq-code/day7/練習.py", line 7, in foo
raise FooError(‘invalid value: %s‘ % s)
__main__.FooError: invalid value: 0
註:只有在必要的時候才定義我們自己的錯誤類型。如果可以選擇Python已有的內置的錯誤類型(比如ValueError
,TypeError
),盡量使用Python內置的錯誤類型
def foo(s):
n = int(s)
if n==0:
raise ValueError(‘invalid value: %s‘ % s)
return 10 / n
def bar():
try:
foo(‘0‘)
except ValueError as e:
print(‘ValueError!‘)
raise
bar()
輸出結果:
ValueError!
Traceback (most recent call last):
File "C:/Users/lidal/PycharmProjects/llq-code/day7/練習.py", line 14, in <module>
bar()
File "C:/Users/lidal/PycharmProjects/llq-code/day7/練習.py", line 9, in bar
foo(‘0‘)
File "C:/Users/lidal/PycharmProjects/llq-code/day7/練習.py", line 4, in foo
raise ValueError(‘invalid value: %s‘ % s)
ValueError: invalid value: 0
在bar()
函數中,我們明明已經捕獲了錯誤,但是,打印一個ValueError!
後,又把錯誤通過raise
語句拋出去了
捕獲錯誤目的只是記錄一下,便於後續追蹤。但是,由於當前函數不知道應該怎麽處理該錯誤,所以,最恰當的方式是繼續往上拋,讓頂層調用者去處理。
好比一個員工處理不了一個問題時,就把問題拋給他的老板,如果他的老板也處理不了,就一直往上拋,最終會拋給CEO去處理。
raise
語句如果不帶參數,就會把當前錯誤原樣拋出。此外,在except
中raise
一個Error,還可以把一種類型的錯誤轉化成另一種類型:
def main():
try:
10 / 0
except ZeroDivisionError:
raise ValueError(‘input error!‘)
main()
輸出結果:
Traceback (most recent call last):
File "C:/Users/lidal/PycharmProjects/llq-code/day7/練習.py", line 3, in main
10 / 0
ZeroDivisionError: division by zero
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "C:/Users/lidal/PycharmProjects/llq-code/day7/練習.py", line 6, in <module>
main()
File "C:/Users/lidal/PycharmProjects/llq-code/day7/練習.py", line 5, in main
raise ValueError(‘input error!‘)
ValueError: input error!
只要是合理的轉換邏輯就可以,但是,決不應該把一個IOError
轉換成毫不相幹的ValueError
。
小結
Python內置的try...except...finally
用來處理錯誤十分方便。出錯時,會分析錯誤信息並定位錯誤發生的代碼位置才是最關鍵的。
程序也可以主動拋出錯誤,讓調用者來處理相應的錯誤。但是,應該在文檔中寫清楚可能會拋出哪些錯誤,以及錯誤產生的原因。
Python 異常處理