最後,我們來看另一種錯誤處理的方式:
defbar():try:
foo('0')exceptValueError as e:print('ValueError!')raise
在bar()函數中,我們明明已經捕獲了錯誤,但是,列印一個ValueError!後,又把錯誤通過raise語句抛出去了
捕獲錯誤目的隻是記錄一下,便于後續追蹤。但是,由于目前函數不知道應該怎麼處理該錯誤,是以,最恰當的方式是繼續往上抛,讓頂層調用者去處理。好比一個員工處理不了一個問題時,就把問題抛給他的老闆,如果他的老闆也處理不了,就一直往上抛,最終會抛給CEO去處理。
raise語句如果不帶參數,就會把目前錯誤原樣抛出。此外,在except中raise一個Error,還可以把一種類型的錯誤轉化成另一種類型:
try:10 /0exceptZeroDivisionError:raise ValueError('input error!')
斷言
凡是用print()來輔助檢視的地方,都可以用斷言(assert)來替代:
deffoo(s):
n=int(s)assert n != 0, 'n is zero!'
return 10 /ndefmain():
foo('0')
assert的意思是,表達式 n != 0應該是True,否則,根據程式運作的邏輯,後面的代碼肯定會出錯。
如果斷言失敗,assert語句本身就會抛出AssertionError:
程式中如果到處充斥着assert,和print()相比也好不到哪去。不過,啟動Python解釋器時可以用-O參數來關閉assert:
$ python3 -O err.py
關閉後,你可以把所有的assert語句當成pass來看。
logging
把print()替換為logging是第3種方式,和assert比,logging不會抛出錯誤,而且可以輸出到檔案:
importlogging
s= '0'n=int(s)
logging.info('n = %d' %n)print(10 / n)
logging.info()就可以輸出一段文本。運作,發現除了ZeroDivisionError,沒有任何資訊。怎麼回事?
别急,在import logging之後添加一行配置再試試:
importlogging
logging.basicConfig(level=logging.INFO)
看到輸出了:
$ python3 err.py
INFO:root:n=0
Traceback (most recent call last):
File"err.py", line 8, in
print(10 /n)
ZeroDivisionError: division by zero
這就是logging的好處,它允許你指定記錄資訊的級别,有debug,info,warning,error等幾個級别,當我們指定level=INFO時,logging.debug就不起作用了。同理,指定level=WARNING後,debug和info就不起作用了。這樣一來,你可以放心地輸出不同級别的資訊,也不用删除,最後統一控制輸出哪個級别的資訊。
logging的另一個好處是通過簡單的配置,一條語句可以同時輸出到不同的地方,比如console和檔案。
pdb
第4種方式是啟動Python的調試器pdb,讓程式以單步方式運作,可以随時檢視運作狀态。我們先準備好程式:
#err.py
s = '0'n=int(s)print(10 / n)
然後啟動:
$ python3 -m pdb err.py> /Users/michael/Github/learn-python3/samples/debug/err.py(2)()-> s = '0'
以參數-m pdb啟動後,pdb定位到下一步要執行的代碼-> s = '0'。輸入指令l來檢視代碼:
(Pdb) l1 #err.py
2 -> s = '0'
3 n =int(s)4 print(10 / n)
輸入指令n可以單步執行代碼:
(Pdb) n> /Users/michael/Github/learn-python3/samples/debug/err.py(3)()-> n =int(s)
(Pdb) n> /Users/michael/Github/learn-python3/samples/debug/err.py(4)()-> print(10 / n)
任何時候都可以輸入指令p 變量名來檢視變量:
(Pdb) p s'0'(Pdb) p n
輸入指令q結束調試,退出程式:
(Pdb) q
這種通過pdb在指令行調試的方法理論上是萬能的,但實在是太麻煩了,如果有一千行代碼,要運作到第999行得敲多少指令啊。還好,我們還有另一種調試方法。
pdb.set_trace()
這個方法也是用pdb,但是不需要單步執行,我們隻需要import pdb,然後,在可能出錯的地方放一個pdb.set_trace(),就可以設定一個斷點:
#err.py
importpdb
s= '0'n=int(s)
pdb.set_trace()#運作到這裡會自動暫停
print(10 / n)
運作代碼,程式會自動在pdb.set_trace()暫停并進入pdb調試環境,可以用指令p檢視變量,或者用指令c繼續運作:
$ python3 err.py> /Users/michael/Github/learn-python3/samples/debug/err.py(7)()-> print(10 /n)
(Pdb) p n
(Pdb) c
Traceback (most recent call last):
File"err.py", line 7, in
print(10 /n)
ZeroDivisionError: division by zero
這個方式比直接啟動pdb單步調試效率要高很多,但也高不到哪去。
IDE
如果要比較爽地設定斷點、單步執行,就需要一個支援調試功能的IDE。目前比較好的Python IDE有PyCharm:
另外,Eclipse加上pydev插件也可以調試Python程式。
雖然用IDE調試起來比較友善,但是最後你會發現,logging才是終極武器。
IDE
單元測試是用來對一個子產品、一個函數或者一個類來進行正确性檢驗的測試工作。
比如對函數abs(),我們可以編寫出以下幾個測試用例:
輸入正數,比如1、1.2、0.99,期待傳回值與輸入相同;
輸入負數,比如-1、-1.2、-0.99,期待傳回值與輸入相反;
輸入0,期待傳回0;
輸入非數值類型,比如None、[]、{},期待抛出TypeError。
把上面的測試用例放到一個測試子產品裡,就是一個完整的單元測試。
如果單元測試通過,說明我們測試的這個函數能夠正常工作。如果單元測試不通過,要麼函數有bug,要麼測試條件輸入不正确,總之,需要修複使單元測試能夠通過。
單元測試通過後有什麼意義呢?如果我們對abs()函數代碼做了修改,隻需要再跑一遍單元測試,如果通過,說明我們的修改不會對abs()函數原有的行為造成影響,如果測試不通過,說明我們的修改與原有行為不一緻,要麼修改代碼,要麼修改測試。
這種以測試為驅動的開發模式最大的好處就是確定一個程式子產品的行為符合我們設計的測試用例。在将來修改的時候,可以極大程度地保證該子產品行為仍然是正确的。
我們來編寫一個Dict類,這個類的行為和dict一緻,但是可以通過屬性來通路,用起來就像下面這樣:
>>> d = Dict(a=1, b=2)>>> d['a'] #1
>>> d.a #1
classDict(dict):def __init__(self, **kw):
super().__init__(**kw)def __getattr__(self, key):try:returnself[key]exceptKeyError:raise AttributeError(r"'Dict' object has no attribute '%s'" %key)def __setattr__(self, key, value):
self[key]= value
為了編寫單元測試,我們需要引入Python自帶的unittest子產品,編寫mydict_test.py如下:
importunittestfrom mydict importDictclassTestDict(unittest.TestCase):deftest_init(self):
d= Dict(a=1, b='test')
self.assertEqual(d.a,1)
self.assertEqual(d.b,'test')
self.assertTrue(isinstance(d, dict))deftest_key(self):
d=Dict()
d['key'] = 'value'self.assertEqual(d.key,'value')deftest_attr(self):
d=Dict()
d.key= 'value'self.assertTrue('key' ind)
self.assertEqual(d['key'], 'value')deftest_keyerror(self):
d=Dict()
with self.assertRaises(KeyError):
value= d['empty']deftest_attrerror(self):
d=Dict()
with self.assertRaises(AttributeError):
value= d.empty
編寫單元測試時,我們需要編寫一個測試類,從unittest.TestCase繼承。
以test開頭的方法就是測試方法,不以test開頭的方法不被認為是測試方法,測試的時候不會被執行。
對每一類測試都需要編寫一個test_xxx()方法。由于unittest.TestCase提供了很多内置的條件判斷,我們隻需要調用這些方法就可以斷言輸出是否是我們所期望的。最常用的斷言就是assertEqual():
self.assertEqual(abs(-1), 1) #斷言函數傳回的結果與1相等
另一種重要的斷言就是期待抛出指定類型的Error,比如通過d['empty']通路不存在的key時,斷言會抛出KeyError:
with self.assertRaises(KeyError):
value= d['empty']
而通過d.empty通路不存在的key時,我們期待抛出AttributeError:
with self.assertRaises(AttributeError):
value= d.empty
運作單元測試
一旦編寫好單元測試,我們就可以運作單元測試。最簡單的運作方式是在mydict_test.py的最後加上兩行代碼:
if __name__ == '__main__':
unittest.main()
這樣就可以把mydict_test.py當做正常的python腳本運作:
$ python3 mydict_test.py
另一種方法是在指令行通過參數-m unittest直接運作單元測試:
$ python3 -m unittest mydict_test
.....----------------------------------------------------------------------Ran5 tests in0.000s
OK
這是推薦的做法,因為這樣可以一次批量運作很多單元測試,并且,有很多工具可以自動來運作這些單元測試。
setUp與tearDown
可以在單元測試中編寫兩個特殊的setUp()和tearDown()方法。這兩個方法會分别在每調用一個測試方法的前後分别被執行。
setUp()和tearDown()方法有什麼用呢?設想你的測試需要啟動一個資料庫,這時,就可以在setUp()方法中連接配接資料庫,在tearDown()方法中關閉資料庫,這樣,不必在每個測試方法中重複相同的代碼:
classTestDict(unittest.TestCase):defsetUp(self):print('setUp...')deftearDown(self):print('tearDown...')
可以再次運作測試看看每個測試方法調用前後是否會列印出setUp...和tearDown...。
單元測試的測試用例要覆寫常用的輸入組合、邊界條件和異常。
文檔測試
如果你經常閱讀Python的官方文檔,可以看到很多文檔都有示例代碼。比如re子產品就帶了很多示例代碼:
>>> importre>>> m = re.search('(?<=abc)def', 'abcdef')>>>m.group(0)'def'
可以把這些示例代碼在Python的互動式環境下輸入并執行,結果與文檔中的示例代碼顯示的一緻。
這些代碼與其他說明可以寫在注釋中,然後,由一些工具來自動生成文檔。既然這些代碼本身就可以粘貼出來直接運作,那麼,可不可以自動執行寫在注釋中的這些代碼呢?
答案是肯定的。
當我們編寫注釋時,如果寫上這樣的注釋:
defabs(n):'''Function to get absolute value of number.
Example:
>>> abs(1)
1
>>> abs(-1)
1
>>> abs(0)
0'''
return n if n >= 0 else (-n)
無疑更明确地告訴函數的調用者該函數的期望輸入和輸出。
并且,Python内置的“文檔測試”(doctest)子產品可以直接提取注釋中的代碼并執行測試。
doctest嚴格按照Python互動式指令行的輸入和輸出來判斷測試結果是否正确。隻有測試異常的時候,可以用...表示中間一大段煩人的輸出。
讓我們用doctest來測試上次編寫的Dict類:
#mydict2.py
classDict(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:returnself[key]exceptKeyError:raise AttributeError(r"'Dict' object has no attribute '%s'" %key)def __setattr__(self, key, value):
self[key]=valueif __name__=='__main__':importdoctest
doctest.testmod()
運作python3 mydict2.py:
$ python3 mydict2.py
什麼輸出也沒有。這說明我們編寫的doctest運作都是正确的。如果程式有問題,比如把__getattr__()方法注釋掉,再運作就會報錯:
當子產品正常導入時,doctest不會被執行。隻有在指令行直接運作時,才執行doctest。是以,不必擔心doctest會在非測試環境下執行。
doctest非常有用,不但可以用來測試,還可以直接作為示例代碼。通過某些文檔生成工具,就可以自動把包含doctest的注釋提取出來。使用者看文檔的時候,同時也看到了doctest。