錯誤處理
在程序運行的過程中顾彰,如果發(fā)生了錯誤极阅,可以事先約定返回一個錯誤代碼,這樣涨享,就可以知道是否有錯筋搏,以及出錯的原因。在操作系統(tǒng)提供的調(diào)用中厕隧,返回錯誤碼非常常見奔脐。比如打開文件的函數(shù)open(),成功時返回文件描述符(就是一個整數(shù))吁讨,出錯時返回-1
髓迎。
用錯誤碼來表示是否出錯十分不便,因為函數(shù)本身應(yīng)該返回的正常結(jié)果和錯誤碼混在一起建丧,造成調(diào)用者必須用大量的代碼來判斷是否出錯.
try
一旦出錯排龄,還要一級一級上報,直到某個函數(shù)可以處理該錯誤(比如翎朱,給用戶輸出一個錯誤信息)橄维。
所以高級語言通常都內(nèi)置了一套try...except...finally...
的錯誤處理機制,Python也不例外拴曲。
用一個例子來看看try
的機制:
try:
print('try...')
r = 10 / 0
print('result:', r)
except ZeroDivisionError as e:
print('except:', e)
finally:
print('finally...')
print('END')
當我們認為某些代碼可能會出錯時争舞,就可以用try
來運行這段代碼,如果執(zhí)行出錯澈灼,則后續(xù)代碼不會繼續(xù)執(zhí)行竞川,而是直接跳轉(zhuǎn)至錯誤處理代碼,即except
語句塊叁熔,執(zhí)行完except
后委乌,如果有finally
語句塊,則執(zhí)行finally
語句塊者疤,至此福澡,執(zhí)行完畢。
上面的代碼在計算10 / 0
時會產(chǎn)生一個除法運算錯誤:
try...
except: division by zero
finally...
END
從輸出可以看到驹马,當錯誤發(fā)生時革砸,后續(xù)語句print('result:', r)
不會被執(zhí)行,except
由于捕獲到ZeroDivisionError
糯累,因此被執(zhí)行算利。最后,finally
語句被執(zhí)行泳姐。然后效拭,程序繼續(xù)按照流程往下走。
此外胖秒,如果沒有錯誤發(fā)生缎患,可以在except
語句塊后面加一個else
,當沒有錯誤發(fā)生時阎肝,會自動執(zhí)行else
語句:
try:
print('try...')
r = 10 / int('2')
print('result:', r)
except ValueError as e:
print('ValueError:', e)
except ZeroDivisionError as e:
print('ZeroDivisionError:', e)
else:
print('no error!')
finally:
print('finally...')
print('END')
Python的錯誤其實也是class挤渔,所有的錯誤類型都繼承自BaseException
,所以在使用except
時需要注意的是风题,它不但捕獲該類型的錯誤判导,還把其子類也“一網(wǎng)打盡”:
try:
foo()
except ValueError as e:
print('ValueError')
except UnicodeError as e:
print('UnicodeError')
第二個except永遠也捕獲不到UnicodeError,因為UnicodeError是ValueError的子類沛硅,如果有眼刃,也被第一個except給捕獲了。
調(diào)用棧
如果錯誤沒有被捕獲摇肌,它就會一直往上拋擂红,最后被Python解釋器捕獲,打印一個錯誤信息围小,然后程序退出昵骤。來看看err.py
:
# err.py:
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
bar('0')
main()
#執(zhí)行,結(jié)果如下
$ python3 err.py
Traceback (most recent call last):
File "err.py", line 11, in <module>
main()
File "err.py", line 9, in main
bar('0')
File "err.py", line 6, in bar
return foo(s) * 2
File "err.py", line 3, in foo
return 10 / int(s)
ZeroDivisionError: division by zero
出錯并不可怕吩抓,可怕的是不知道哪里出錯了涉茧。解讀錯誤信息是定位錯誤的關(guān)鍵。我們從上往下可以看到整個錯誤的調(diào)用函數(shù)鏈:
Traceback (most recent call last):#錯誤信息第1行,告訴我們這是錯誤的跟蹤信息疹娶。
第2~3行:
File "err.py", line 11, in <module>
main()
調(diào)用main()
出錯了伴栓,在代碼文件err.py的第11行代碼,但原因是第9行:
File "err.py", line 9, in main
bar('0')
調(diào)用bar('0')
出錯了雨饺,在代碼文件err.py的第9行代碼钳垮,但原因是第6行:
File "err.py", line 6, in bar
return foo(s) * 2
原因是return foo(s) * 2
這個語句出錯了,但這還不是最終原因额港,繼續(xù)往下看:
File "err.py", line 3, in foo
return 10 / int(s)
原因是return 10 / int(s)
這個語句出錯了饺窿,這是錯誤產(chǎn)生的源頭,因為下面打印了:
ZeroDivisionError: integer division or modulo by zero
根據(jù)錯誤類型ZeroDivisionError
移斩,我們判斷肚医,int(s)
本身并沒有出錯绢馍,但是int(s)
返回0,在計算10 / 0
時出錯肠套,至此舰涌,找到錯誤源頭。
記錄錯誤
如果不捕獲錯誤你稚,自然可以讓Python解釋器來打印出錯誤堆棧瓷耙,但程序也被結(jié)束了。既然我們能捕獲錯誤刁赖,就可以把錯誤堆棧打印出來搁痛,然后分析錯誤原因,同時宇弛,讓程序繼續(xù)執(zhí)行下去鸡典。
Python內(nèi)置的logging
模塊可以非常容易地記錄錯誤信息:
# err_logging.py
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')
同樣是出錯,但程序打印完錯誤信息后會繼續(xù)執(zhí)行涯肩,并正常退出:
$ python3 err_logging.py
ERROR:root:division by zero
Traceback (most recent call last):
File "err_logging.py", line 13, in main
bar('0')
File "err_logging.py", line 9, in bar
return foo(s) * 2
File "err_logging.py", line 6, in foo
return 10 / int(s)
ZeroDivisionError: division by zero
END
拋出錯誤
略
調(diào)試
程序能一次寫完并正常運行的概率很小轿钠,基本不超過1%姜贡×崦粒總會有各種各樣的bug需要修正。有的bug很簡單宾肺,看看錯誤信息就知道硫朦,有的bug很復(fù)雜贷腕,我們需要知道出錯時,哪些變量的值是正確的咬展,哪些變量的值是錯誤的泽裳,因此,需要一整套調(diào)試程序的手段來修復(fù)bug破婆。
第一種方法簡單直接粗暴有效涮总,就是用print()
把可能有問題的變量打印出來看看:
def foo(s):
n = int(s)
print('>>> n = %d' % n)
return 10 / n
def main():
foo('0')
main()
執(zhí)行后在輸出中查找打印的變量值:
$ python err.py
>>> n = 0
Traceback (most recent call last):
...
ZeroDivisionError: integer division or modulo by zero
用print()
最大的壞處是將來還得刪掉它,想想程序里到處都是print()
祷舀,運行結(jié)果也會包含很多垃圾信息瀑梗。所以,我們又有第二種方法裳扯。
斷言
凡是用print()
來輔助查看的地方抛丽,都可以用斷言(assert)來替代.
logging / pdb / pdb.set_trace() / IDE
單元測試
如果你聽說過“測試驅(qū)動開發(fā)”(TDD:Test-Driven Development),單元測試就不陌生饰豺。
單元測試是用來對一個模塊亿鲜、一個函數(shù)或者一個類來進行正確性檢驗的測試工作。
mydict.py
代碼如下:
class Dict(dict):
def __init__(self, **kw):
super().__init__(**kw)
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(r"'Dict' object has no attribute '%s'" % key)
def __setattr__(self, key, value):
self[key] = value
為了編寫單元測試冤吨,我們需要引入Python自帶的unittest
模塊蒿柳,編寫mydict_test.py
如下:
import unittest
from mydict import Dict
class TestDict(unittest.TestCase):
def test_init(self):
d = Dict(a=1, b='test')
self.assertEqual(d.a, 1)
self.assertEqual(d.b, 'test')
self.assertTrue(isinstance(d, dict))
def test_key(self):
d = Dict()
d['key'] = 'value'
self.assertEqual(d.key, 'value')
def test_attr(self):
d = Dict()
d.key = 'value'
self.assertTrue('key' in d)
self.assertEqual(d['key'], 'value')
def test_keyerror(self):
d = Dict()
with self.assertRaises(KeyError):
value = d['empty']
def test_attrerror(self):
d = Dict()
with self.assertRaises(AttributeError):
value = d.empty
編寫單元測試時饶套,我們需要編寫一個測試類,從unittest.TestCase
繼承其馏。
以test
開頭的方法就是測試方法凤跑,不以test
開頭的方法不被認為是測試方法爆安,測試的時候不會被執(zhí)行叛复。
對每一類測試都需要編寫一個test_xxx()
方法。由于unittest.TestCase
提供了很多內(nèi)置的條件判斷扔仓,我們只需要調(diào)用這些方法就可以斷言輸出是否是我們所期望的褐奥。最常用的斷言就是assertEqual()
:
self.assertEqual(abs(-1), 1) # 斷言函數(shù)返回的結(jié)果與1相等
運行單元測試
一旦編寫好單元測試,我們就可以運行單元測試翘簇。最簡單的運行方式是在mydict_test.py
的最后加上兩行代碼:
if __name__ == '__main__':
unittest.main()
這樣就可以把mydict_test.py
當做正常的python腳本運行:
$ python mydict_test.py
setUp與tearDown
可以在單元測試中編寫兩個特殊的setUp()
和tearDown()
方法撬码。這兩個方法會分別在每調(diào)用一個測試方法的前后分別被執(zhí)行。
setUp()
和tearDown()
方法有什么用呢版保?設(shè)想你的測試需要啟動一個數(shù)據(jù)庫呜笑,這時,就可以在setUp()
方法中連接數(shù)據(jù)庫彻犁,在tearDown()
方法中關(guān)閉數(shù)據(jù)庫叫胁,這樣,不必在每個測試方法中重復(fù)相同的代碼:
class TestDict(unittest.TestCase):
def setUp(self):
print('setUp...')
def tearDown(self):
print('tearDown...')
可以再次運行測試看看每個測試方法調(diào)用前后是否會打印出setUp...
和tearDown...
汞幢。
# mydict2.py
class Dict(dict):
'''
Simple dict but also support access as x.y style.
>>> d1 = Dict()
>>> d1['x'] = 100
>>> d1.x
100
>>> d1.y = 200
>>> d1['y']
200
>>> d2 = Dict(a=1, b=2, c='3')
>>> d2.c
'3'
>>> d2['empty']
Traceback (most recent call last):
...
KeyError: 'empty'
>>> d2.empty
Traceback (most recent call last):
...
AttributeError: 'Dict' object has no attribute 'empty'
'''
def __init__(self, **kw):
super(Dict, self).__init__(**kw)
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(r"'Dict' object has no attribute '%s'" % key)
def __setattr__(self, key, value):
self[key] = value
if __name__=='__main__':
import doctest
doctest.testmod()
什么輸出也沒有驼鹅。這說明我們編寫的doctest運行都是正確的。