錯誤、調(diào)試和測試

錯誤處理

在程序運行的過程中顾彰,如果發(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運行都是正確的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末森篷,一起剝皮案震驚了整個濱河市输钩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌仲智,老刑警劉巖买乃,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異钓辆,居然都是意外死亡剪验,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門岩馍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來碉咆,“玉大人,你說我怎么就攤上這事蛀恩∫咄” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵双谆,是天一觀的道長壳咕。 經(jīng)常有香客問我席揽,道長,這世上最難降的妖魔是什么谓厘? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任幌羞,我火速辦了婚禮,結(jié)果婚禮上竟稳,老公的妹妹穿的比我還像新娘属桦。我一直安慰自己,他們只是感情好他爸,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布聂宾。 她就那樣靜靜地躺著,像睡著了一般诊笤。 火紅的嫁衣襯著肌膚如雪系谐。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天讨跟,我揣著相機與錄音纪他,去河邊找鬼。 笑死晾匠,一個胖子當著我的面吹牛茶袒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播混聊,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼弹谁,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了句喜?” 一聲冷哼從身側(cè)響起预愤,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎咳胃,沒想到半個月后植康,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡展懈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年销睁,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片存崖。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡冻记,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出来惧,到底是詐尸還是另有隱情冗栗,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站隅居,受9級特大地震影響钠至,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜胎源,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一棉钧、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧涕蚤,春花似錦宪卿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至申钩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瘪阁,已是汗流浹背撒遣。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留管跺,地道東北人义黎。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像豁跑,于是被迫代替她去往敵國和親廉涕。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容