Python day 9(5) 錯誤處理
一:錯誤、調試和測試
1 程序運行中的錯誤:
a 程序編寫有問題造成的,這種錯誤我們通常稱之為bug,bug是必須修復的。
b 用戶輸入造成的,可以通過檢查用戶輸入來做相應的處理。
c 還有一類錯誤是完全無法在程序運行過程中預測的,比如寫入文件的時候,磁盤滿了,寫不進去了,或者從網絡抓取數據,網絡突然斷掉了。這類錯誤也稱為異常,在程序中通常是必須處理的,否則,程序會因為各種問題終止並退出。
2 調試:我們也需要跟蹤程序的執行,查看變量的值是否正確,這個過程稱為調試。Python的pdb可以讓我們以單步方式執行代碼。
3 測試:最後,編寫測試也很重要。有了良好的測試,就可以在程序修改後反復運行,確保程序輸出符合我們編寫的測試。
二:錯誤處理(用try......except......finally)
1 (用try......except......finally來捕獲錯誤)
1 try: 2 print(‘try...‘) 3 r = 10 / 0 4 print(‘result:‘, r) 5 except ZeroDivisionError as e: 6 print(‘except:‘, e) 7 finally: 8 print(‘finally...‘) 9 print(‘END‘)
a 當我們認為某些代碼可能會出錯時,就可以用try
來運行這段代碼,如果執行出錯,則後續代碼不會繼續執行,而是直接跳轉至錯誤處理代碼,即except
except
後,如果有finally
語句塊,則執行finally
語句塊,至此,執行完畢。
b 錯誤可能有很多種類,如果發生了不同類型的錯誤,應該由不同的except
語句塊處理。沒錯,可以有多個except
來捕獲不同類型的錯誤:
int()
函數可能會拋出ValueError
,所以我們用一個except
捕獲ValueError
,用另一個except
捕獲ZeroDivisionError
。
此外,如果沒有錯誤發生,可以在except
語句塊後面加一個else
,當沒有錯誤發生時,會自動執行else
語句:
1 try: 2 print(‘try...‘)3 r = 10 / int(‘2‘) 4 print(‘result:‘, r) 5 except ValueError as e: 6 print(‘ValueError:‘, e) 7 except ZeroDivisionError as e: 8 print(‘ZeroDivisionError:‘, e) 9 else: 10 print(‘no error!‘) 11 finally: 12 print(‘finally...‘) 13 print(‘END‘)
c Python的錯誤其實也是class,所有的錯誤類型都繼承自BaseException
,所以在使用except
時需要註意的是,它不但捕獲該類型的錯誤,還把其子類也“一網打盡”。比如:
1 try: 2 foo() 3 except ValueError as e: 4 print(‘ValueError‘) 5 except UnicodeError as e: 6 print(‘UnicodeError‘)
第二個except
永遠也捕獲不到UnicodeError
,因為UnicodeError
是ValueError
的子類,如果有,也被第一個except
給捕獲了。
Python所有的錯誤都是從BaseException
類派生的,常見的錯誤類型和繼承關系看這裏:
https://docs.python.org/3/library/exceptions.html#exception-hierarchy
d 使用try...except
捕獲錯誤還有一個巨大的好處,就是可以跨越多層調用,比如函數main()
調用foo()
,foo()
調用bar()
,結果bar()
出錯了,這時,只要main()
捕獲到了,就可以處理:
1 def foo(s): 2 return 10 / int(s) 3 4 def bar(s): 5 return foo(s) * 2 6 7 def main(): 8 try: 9 bar(‘0‘) 10 except Exception as e: 11 print(‘Error:‘, e) 12 finally: 13 print(‘finally...‘)
也就是說,不需要在每個可能出錯的地方去捕獲錯誤,只要在合適的層次去捕獲錯誤就可以了。這樣一來,就大大減少了寫try...except...finally
的麻煩。
2 調用棧(不用try......except......finally來捕獲)
如果錯誤沒有被捕獲,它就會一直往上拋,最後被Python解釋器捕獲,打印一個錯誤信息,然後程序退出。
1 def foo(s): 2 return 10 / int(s) 3 4 def bar(s): 5 return foo(s) * 2 6 7 def main(): 8 bar(‘0‘) 9 10 main()
結果如下:
1 $ python3 err.py 2 Traceback (most recent call last): 3 File "err.py", line 11, in <module> 4 main() 5 File "err.py", line 9, in main 6 bar(‘0‘) 7 File "err.py", line 6, in bar 8 return foo(s) * 2 9 File "err.py", line 3, in foo 10 return 10 / int(s) 11 ZeroDivisionError: division by zero
不過要自己分析錯誤的調用棧信息,定位錯誤的位置。
3 記錄錯誤
如果不捕獲錯誤,自然可以讓Python解釋器來打印出錯誤堆棧,但程序也被結束了。既然我們能捕獲錯誤,就可以把錯誤堆棧打印出來,然後分析錯誤原因,同時,讓程序繼續執行下去。
Python內置的logging
模塊可以非常容易地記錄錯誤信息:
1 import logging 2 3 def foo(s): 4 return 10 / int(s) 5 6 def bar(s): 7 return foo(s) * 2 8 9 def main(): 10 try: 11 bar(‘0‘) 12 except Exception as e: 13 logging.exception(e) 14 15 main() 16 print(‘END‘)
同樣是出錯,但程序打印完錯誤信息後會繼續執行,並正常退出
1 $ python3 err_logging.py 2 ERROR:root:division by zero 3 Traceback (most recent call last): 4 File "err_logging.py", line 13, in main 5 bar(‘0‘) 6 File "err_logging.py", line 9, in bar 7 return foo(s) * 2 8 File "err_logging.py", line 6, in foo 9 return 10 / int(s) 10 ZeroDivisionError: division by zero 11 END
通過配置,logging
還可以把錯誤記錄到日誌文件裏,方便事後排查。
4 拋出錯誤
因為錯誤是class,捕獲一個錯誤就是捕獲到該class的一個實例。因此,錯誤並不是憑空產生的,而是有意創建並拋出的。Python的內置函數會拋出很多類型的錯誤,我們自己編寫的函數也可以拋出錯誤。
如果要拋出錯誤,首先根據需要,可以定義一個錯誤的class,選擇好繼承關系,然後,用raise
語句拋出一個錯誤的實例
1 # err_raise.py 2 class FooError(ValueError): 3 pass 4 5 def foo(s): 6 n = int(s) 7 if n==0: 8 raise FooError(‘invalid value: %s‘ % s) 9 return 10 / n 10 11 foo(‘0‘)
執行,可以最後跟蹤到我們自己定義的錯誤
1 $ python3 err_raise.py 2 Traceback (most recent call last): 3 File "err_throw.py", line 11, in <module> 4 foo(‘0‘) 5 File "err_throw.py", line 8, in foo 6 raise FooError(‘invalid value: %s‘ % s) 7 __main__.FooError: invalid value: 0
只有在必要的時候才定義我們自己的錯誤類型。如果可以選擇Python已有的內置的錯誤類型(比如ValueError
,TypeError
),盡量使用Python內置的錯誤類型。
Python day 9(5) 錯誤處理