python-復盤-錯誤/調試/測試

錯誤處理

try...except...finally...

try:
    print('try...')      #  try except finally 簡單套路
    r = 10 / int('a')
    print('result:', r)
except ValueError as e:
    print('ValueError:', e)
except ZeroDivisionError as e:
    print('ZeroDivisionError:', e)
finally:
    print('finally...')
print('END')


調用堆棧

如果錯誤沒有被捕獲泪电,它就會一直往上拋,最后被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í)行鲜锚,結果如下:

$ 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

出錯并不可怕突诬,可怕的是不知道哪里出錯了。解讀錯誤信息是定位錯誤的關鍵芜繁。我們從上往下可以看到整個錯誤的調用函數鏈:

錯誤信息第1行:

Traceback (most recent call last):

告訴我們這是錯誤的跟蹤信息旺隙。
第2~3行:

  File "err.py", line 11, in <module>
    main()

調用main()出錯了,在代碼文件err.py的第11行代碼骏令,但原因是第9行:

............省略.就是上游污染下游都污染蔬捷,都PM爆表,系統把所有污染地區(qū)都展示出來榔袋,你順著往上游去周拐,就能摸到污染源.............

原因是return foo(s) * 2這個語句出錯了,但這還不是最終原因凰兑,繼續(xù)往下看:

  File "err.py", line 3, in foo
    return 10 / int(s)

原因是return 10 / int(s)這個語句出錯了妥粟,這是錯誤產生的源頭,因為下面打印了:

ZeroDivisionError: integer division or modulo by zero

根據錯誤類型ZeroDivisionError吏够,我們判斷勾给,int(s)本身并沒有出錯,但是int(s)返回0锅知,在計算10 / 0時出錯播急,至此,找到錯誤源頭售睹。


記錄錯誤 logging

如果不捕獲錯誤旅择,自然可以讓Python解釋器來打印出錯誤堆棧,但程序也被結束了侣姆。既然我們能捕獲錯誤生真,就可以把錯誤堆棧打印出來沉噩,然后分析錯誤原因,同時柱蟀,讓程序繼續(xù)執(zhí)行下去川蒙。

Python內置的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

通過配置畜眨,logging還可以把錯誤記錄到日志文件里,方便事后排查术瓮。


拋出錯誤

因為錯誤是class康聂,捕獲一個錯誤就是捕獲到該class的一個實例。因此胞四,錯誤并不是憑空產生的恬汁,而是有意創(chuàng)建并拋出的。Python的內置函數會拋出很多類型的錯誤辜伟,我們自己編寫的函數也可以拋出錯誤氓侧。

如果要拋出錯誤,首先根據需要导狡,可以定義一個錯誤的class约巷,選擇好繼承關系,然后旱捧,用raise語句拋出一個錯誤的實例:

# err_raise.py
class FooError(ValueError):
    pass

def foo(s):
    n = int(s)
    if n==0:
        raise FooError('invalid value: %s' % s)
    return 10 / n

foo('0')

執(zhí)行独郎,可以最后跟蹤到我們自己定義的錯誤:

$ python3 err_raise.py 
Traceback (most recent call last):
  File "err_throw.py", line 11, in <module>
    foo('0')
  File "err_throw.py", line 8, in foo
    raise FooError('invalid value: %s' % s)
__main__.FooError: invalid value: 0

只有在必要的時候才定義我們自己的錯誤類型。如果可以選擇Python已有的內置的錯誤類型(比如ValueError枚赡,TypeError)囚聚,盡量使用Python內置的錯誤類型。
最后标锄,我們來看另一種錯誤處理的方式:

# err_reraise.py

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()

bar()函數中,我們明明已經捕獲了錯誤茁计,但是料皇,打印一個ValueError!后,又把錯誤通過raise語句拋出去了星压,這不有病么践剂?

其實這種錯誤處理方式不但沒病,而且相當常見娜膘。捕獲錯誤目的只是記錄一下逊脯,便于后續(xù)追蹤。但是竣贪,由于當前函數不知道應該怎么處理該錯誤军洼,所以巩螃,最恰當的方式是繼續(xù)往上拋,讓頂層調用者去處理匕争。好比一個員工處理不了一個問題時避乏,就把問題拋給他的老板,如果他的老板也處理不了甘桑,就一直往上拋拍皮,最終會拋給CEO去處理。

raise語句如果不帶參數跑杭,就會把當前錯誤原樣拋出铆帽。此外,在exceptraise一個Error德谅,還可以把一種類型的錯誤轉化成另一種類型.



調試

斷言

凡是用print()來輔助查看的地方爹橱,都可以用斷言(assert)來替代:

def foo(s):
    n = int(s)
    assert n != 0, 'n is zero!'
    return 10 / n

def main():
    foo('0')

assert的意思是,表達式n != 0應該是True女阀,否則宅荤,根據程序運行的邏輯,后面的代碼肯定會出錯浸策。

如果斷言失敗冯键,assert語句本身就會拋出AssertionError

$ python3 err.py
Traceback (most recent call last):
  ...
AssertionError: n is zero!

logging

print()替換為logging是第3種方式,和assert比庸汗,logging不會拋出錯誤惫确,而且可以輸出到文件:

import logging

s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10 / n)

logging.info()就可以輸出一段文本。運行蚯舱,發(fā)現除了ZeroDivisionError改化,沒有任何信息。怎么回事枉昏?

別急陈肛,在import logging之后添加一行配置再試試:

import logging
logging.basicConfig(level=logging.INFO)

看到輸出了:

$ python3 err.py
INFO:root:n = 0
Traceback (most recent call last):
  File "err.py", line 8, in <module>
    print(10 / n)
ZeroDivisionError: division by zero

這就是logging的好處,它允許你指定記錄信息的級別兄裂,有debug句旱,info,warning晰奖,error等幾個級別谈撒,當我們指定level=INFO時,logging.debug就不起作用了匾南。同理啃匿,指定level=WARNING后,debug和info就不起作用了。這樣一來溯乒,你可以放心地輸出不同級別的信息夹厌,也不用刪除,最后統一控制輸出哪個級別的信息橙数。

logging的另一個好處是通過簡單的配置尊流,一條語句可以同時輸出到不同的地方,比如console和文件灯帮。

pdb pdb.set_trace() IDE 暫略

單元測試

“測試驅動開發(fā)”(TDD:Test-Driven Development)
單元測試是用來對一個模塊崖技、一個函數或者一個類來進行正確性檢驗的測試工作。

我們來編寫一個Dict類钟哥,這個類的行為和dict一致迎献,但是可以通過屬性來訪問,用起來就像下面這樣:

>>> d = Dict(a=1, b=2)
>>> d['a']
1
>>> d.a
1

為了編寫單元測試腻贰,我們需要引入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提供了很多內置的條件判斷洲炊,我們只需要調用這些方法就可以斷言輸出是否是我們所期望的感局。最常用的斷言就是
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
.....
----------------------------------------------------------------------
Ran 5 tests in 0.000s

OK

這是推薦的做法撑毛,因為這樣可以一次批量運行很多單元測試,并且唧领,有很多工具可以自動來運行這些單元測試藻雌。

setUp與tearDown

可以在單元測試中編寫兩個特殊的setUp()tearDown()方法。這兩個方法會分別在每調用一個測試方法的前后分別被執(zhí)行疹吃。

setUp()tearDown()方法有什么用呢?設想你的測試需要啟動一個數據庫西雀,這時萨驶,就可以在setUp()方法中連接數據庫,在tearDown()方法中關閉數據庫艇肴,這樣腔呜,不必在每個測試方法中重復相同的代碼:

class TestDict(unittest.TestCase):

    def setUp(self):
        print('setUp...')

    def tearDown(self):
        print('tearDown...')

可以再次運行測試看看每個測試方法調用前后是否會打印出setUp...和tearDown...叁温。

文檔測試

如果你經常閱讀Python的官方文檔,可以看到很多文檔都有示例代碼核畴。比如re模塊就帶了很多示例代碼:

>>> import re
>>> m = re.search('(?<=abc)def', 'abcdef')
>>> m.group(0)
'def'

可以把這些示例代碼在Python的交互式環(huán)境下輸入并執(zhí)行膝但,結果與文檔中的示例代碼顯示的一致。

這些代碼與其他說明可以寫在注釋中谤草,然后跟束,由一些工具來自動生成文檔。既然這些代碼本身就可以粘貼出來直接運行丑孩,那么冀宴,可不可以自動執(zhí)行寫在注釋中的這些代碼呢?

答案是肯定的温学。

當我們編寫注釋時略贮,如果寫上這樣的注釋:

def abs(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)模塊可以直接提取注釋中的代碼并執(zhí)行測試逃延。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市轧拄,隨后出現的幾起案子揽祥,更是在濱河造成了極大的恐慌,老刑警劉巖紧帕,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盔然,死亡現場離奇詭異,居然都是意外死亡是嗜,警方通過查閱死者的電腦和手機愈案,發(fā)現死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鹅搪,“玉大人站绪,你說我怎么就攤上這事±鍪粒” “怎么了恢准?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長甫题。 經常有香客問我馁筐,道長,這世上最難降的妖魔是什么坠非? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任敏沉,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘盟迟。我一直安慰自己秋泳,他們只是感情好,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布攒菠。 她就那樣靜靜地躺著迫皱,像睡著了一般。 火紅的嫁衣襯著肌膚如雪辖众。 梳的紋絲不亂的頭發(fā)上卓起,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音赵辕,去河邊找鬼既绩。 笑死,一個胖子當著我的面吹牛还惠,可吹牛的內容都是我干的饲握。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蚕键,長吁一口氣:“原來是場噩夢啊……” “哼救欧!你這毒婦竟也來了?” 一聲冷哼從身側響起锣光,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤笆怠,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后誊爹,有當地人在樹林里發(fā)現了一具尸體蹬刷,經...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年频丘,在試婚紗的時候發(fā)現自己被綠了办成。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡搂漠,死狀恐怖迂卢,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情桐汤,我是刑警寧澤而克,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站怔毛,受9級特大地震影響员萍,放射性物質發(fā)生泄漏。R本人自食惡果不足惜拣度,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一碎绎、第九天 我趴在偏房一處隱蔽的房頂上張望蜂莉。 院中可真熱鬧,春花似錦混卵、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至宿接,卻和暖如春赘淮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背睦霎。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工梢卸, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人副女。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓蛤高,卻偏偏與公主長得像,于是被迫代替她去往敵國和親碑幅。 傳聞我的和親對象是個殘疾皇子戴陡,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內容

  • 高級語言通常都內置了一套try...except...finally...的錯誤處理機制,Python也不例外沟涨。 ...
    時間之友閱讀 755評論 0 1
  • 錯誤處理 在程序運行的過程中裹赴,如果發(fā)生錯誤喜庞,可以事先約定返回一個錯誤代碼,這樣就可以知道是否有錯棋返,以及出錯的原因延都。...
    Sun_atom閱讀 261評論 0 0
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,162評論 25 707
  • mysql 高級語句 一、存儲過程 1.什么是存儲過程: 就是一組SQL語句集懊昨,功能強大窄潭,可以實現一些比較復雜的邏...
    君滿樓001閱讀 3,101評論 0 0
  • 《視而不見》,我的觀后感酵颁。 視而不見似乎很常見嫉你,你這樣,我這樣躏惋,似乎大家都這樣幽污。小孩子的世界這樣,大人...
    格子0510閱讀 839評論 0 0