Pyhton學習筆記九:錯誤、調試和測試

  • 錯誤:
    1、程序編寫有問題造成的:這種錯誤我們通常稱之爲bug,bug是必須修復的。
    2、用戶輸入造成的:這種錯誤可以通過檢查用戶輸入來做相應的處理。
    3、完全無法在程序運行過程中預測的:比如寫入文件的時候,磁盤滿了,寫不進去了,或者從網絡抓取數據,網絡突然斷掉了。這類錯誤也稱爲異常,在程序中通常是必須處理的,否則,程序會因爲各種問題終止並退出。
  • 解決辦法
    1、跟蹤程序的執行,查看變量的值是否正確,這個過程稱爲調試。
    2、Python的pdb可以讓我們以單步方式執行代碼。

1. 錯誤處理

1.1 錯誤代碼

  • 程序運行中,若發生錯誤,可以事先約定返回一個錯誤代碼,例如:打開文件的函數open(),成功時返回文件描述符(一個整數),出錯時返回-1
  • 但是用這種的方式表示是否出錯十分不便,因爲函數本身應該返回的正常結果和錯誤代碼混在一起:
def foo():
    r = some_function()
    if r==(-1):
        return (-1)
    # do something
    return r

def bar():
    r = foo()
    if r==(-1):
        print('Error')
    else:
        pass

1.2 try

  • 按照上面錯誤代碼的方式很容易出錯,故大部分語言都內置了一套try...except...finally...的錯誤處理機制:
# 當我們認爲某些代碼可能會報錯,就將這段代碼放入try中來運行:
try:
    print('try...')
    r = 10 / 0
    print('result:', r)
# 如果執行出錯,後續代碼不會運行,而是直接跳轉至錯誤處理代碼,即:except語句塊:
except ZeroDivisionError as e:
    print('except:', e)
# 執行完except語句後,如果有finally語句,則執行finally語句塊,至此,執行完畢;
finally:
    print('finally...')
print('END')


# 運行結果:
try...
except: division by zero
finally...
END

# 若將10 / 0 改爲 10 / 2,則得:
try...
result: 5.0
finally...
END

# 可能同時有多種錯誤,那就需要由不同的except語句塊捕獲不同的錯誤:
try:
    print('try...')
    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')

# 運行結果
try...
ValueError: invalid literal for int() with base 10: 'a'
finally...
END

# 若沒有錯誤,可在except語句塊後面加個else,沒有錯誤時,自動執行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中錯誤類型其實也是calss,左右的錯誤類型都繼承自BaseException,
# 所以在使用except時,它不但捕獲該類型的錯誤,還將其子類也一網打盡,如:
try:
    foo()
except ValueError as e:
    print('ValueError')
except UnicodeError as e:  # 此except永遠捕獲不到UnicodeError,因爲UnicodeError是ValueError的子類
    print('UnicodeError')

# Python所有的錯誤都是從BaseException類派生的:
# [常見錯誤類型和繼承關係](https://docs.python.org/3/library/exceptions.html#exception-hierarchy)

# 跨越多層調用
# 函數main()調用bar(),bar()調用foo(),結果foo()出錯,這時,只要main()捕獲到,就可以處理,
# 也就是說只需要在合適的層次捕獲錯誤即可,大大減少了寫try...except...finally的麻煩:
def foo(s):
    return 10 / int(s)

def bar(s):
    return foo(s) * 2

def main():
    try:
        bar('0')
    except Exception as e:
        print('Error:', e)
    finally:
        print('finally...')

1.3 調用棧

  • 如果錯誤沒有被捕獲,它就會一直往上拋,最後被Python解釋器捕獲,打印一個錯誤信息,然後程序退出。來看看 err.py
# err.py:
def foo(s):
    return 10 / int(s)

def bar(s):
    return foo(s) * 2

def main():
    bar('0')

main()

執行,結果如下:


$ 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行:
```python
File "err.py", line 9, in main
bar('0')
  • 調用bar('0')出錯了,在代碼文件err.py的第9行代碼,但原因是第6行:
File "err.py", line 6, in bar
return foo(s) * 2
  • 原因是return foo(s) * 2這個語句出錯了,但這還不是最終原因,繼續往下看:
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時出錯,至此,找到錯誤源頭。

1.4 記錄錯誤

  • 既能捕獲錯誤,同時將錯誤堆棧打印出來,讓程序繼續執行下去:
# Python內置的logging模塊可以非常容易的記錄錯誤信息:
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')

# 同樣是出錯,但程序打印完錯誤信息後會繼續執行,並正常退出:

runfile('E:/4_Programe/1_Python/3_Code/1_LiaoDaDa/15_logging.py', wdir='E:/4_Programe/1_Python/3_Code/1_LiaoDaDa')
ERROR:root:division by zero
Traceback (most recent call last):
  File "E:/4_Programe/1_Python/3_Code/1_LiaoDaDa/15_logging.py", line 11, in main
    bar('0')
  File "E:/4_Programe/1_Python/3_Code/1_LiaoDaDa/15_logging.py", line 7, in bar
    return foo(s) * 2
  File "E:/4_Programe/1_Python/3_Code/1_LiaoDaDa/15_logging.py", line 4, in foo
    return 10 / int(s)
ZeroDivisionError: division by zero
END
  • 通過配置,logging還可以把錯誤記錄到日誌文件裏,方便事後排查。

1.5 拋出錯誤

  • 錯誤是class,捕獲一個錯誤就是捕獲到該class的一個實例:
# 定義一個錯誤的class,選擇好繼承關係,用raise語句拋出一個錯誤的實例:
class FooError(ValueError):
    pass

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

foo('0')

# 執行結果
$ 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
  • 另外一種錯誤處理的方式:
# 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()

2. 調試

  • 寫程序時難免會出現bug,故需要一整套調試程序來修復bug:

2.1 print()

def foo(s):
	n = int(s)
	print('>>> n = %d' % n)
	return 10 / n

def main():
	foo('0')

main()

# 運行結果
>>> n = 0
Traceback (most recent call last):
  ...
ZeroDivisionError: integer division or modulo by zero

# 此方法會使程序變得十分臃腫,同時運行結果也會包含很多垃圾信息

2.2 斷言

  • 程序中凡是用print()來輔助查看的地方,都可使用斷言(assert)來替代:
def foo(s):
	n = int(s)
	assert n != 0, 'n is zero!'  # 此句意思爲表達式 n != 0 應該是True,否則根據程序運行的邏輯,後面的代碼肯定出錯
	return 10 / n

def main():
	foo('0')

main()

# 運行結果
Traceback (most recent call last):
  ...
AssertionError: n is zero!   # 如果斷言失敗,assert語句本身會拋出AssertionError
  • 若程序中到處充斥着assert,和print()相比好不到那裏去,故python中允許使用-0參數來關閉assert
$ python -O err.py
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero
# 關閉後,你可以將所有的assert語句當做pass來看

2.3 logging

  • print()替換爲logging,和assert相比,logging不會拋出錯誤,而且可以輸出到文件:
  • logging可以指定記錄信息的級別,有debuginfowarningerror幾個級別,從低到高,當我們指定level=INFO時,logging.debug就不起作用了,這樣,我們就可以放心的輸出不同級別的信息,也不用刪除,最後統一控制輸出那個級別的信息:
import logging
logging.basicConfig(level=logging.INFO)

s = '0'
n = int(s)
logging.info('n = %d' % n)  # 可以輸出一段文本
print(10 / n)

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

2.4 pdb

  • 第四種方式是啓動python的調試器pdb,讓程序以單步方式運行:
# err.py
s = '0'
n = int(s)
print(10 / n)
  • 然後啓動:
E:\4_Programe\1_Python\3_Code\1_LiaoDaDa>python -m pdb err.py
> e:\4_programe\1_python\3_code\1_liaodada\err.py(38)<module>()
-> s = '0'
  • 以參數-m pdb啓動後,pdb定位到下一步要執行的代碼-> s = '0'。輸入命令l來查看代碼:
(Pdb) l
  1     # test.py
  2  -> s = '0'
  3     n = int(s)
  4     print(10 / n)
  • 輸入命令n可以單步執行代碼:
(Pdb) n
> e:\4_programe\1_python\3_code\1_liaodada\test.py(3)<module>()
-> n = int(s)
(Pdb) n
> e:\4_programe\1_python\3_code\1_liaodada\test.py(4)<module>()
-> print(10 / n)
  • 任何時候都可以輸入命令p 變量名來查看變量:
(Pdb) p s
'0'
(Pdb) p n
0
  • 輸入命令q結束調試,退出程序

2.5 pdb.set_trace()

  • 此方法也是pdb,不過不需要單步執行,只需在可能出錯的地方放一個pdb.set_trace(),就可以設置一個斷點:
import pdb

s = '0'
n = int(s)
pdb.set_trace()   # 運行到此會自動暫停
print(10 / n)

# 運行代碼,程序會在pdb.set_trace()暫停並進入pdb調試環境:
runfile('E:/4_Programe/1_Python/3_Code/1_LiaoDaDa/18_err.py', wdir='E:/4_Programe/1_Python/3_Code/1_LiaoDaDa')
> e:\4_programe\1_python\3_code\1_liaodada\18_err.py(51)<module>()
-> print(10 / n)
(Pdb) >? p n
0
(Pdb) >? c
Traceback (most recent call last):
  File "18_err.py", line 51, in <module>
    print(10 / n)
ZeroDivisionError: division by zero
# 此方式雖然比單步pdb效率高,但也不是最佳選擇

2.6 IDE

3. 單元測試

  • 單元測試是用來對一個模塊,一個函數或者一個類來進行正確性檢測的測試工作:
  • 比如對函數abs(),我們可以編寫出以下幾個測試用例:
    1. 輸入正數,比如11.20.99,期待返回值與輸入相同;

    2. 輸入負數,比如-1-1.2-0.99,期待返回值與輸入相反;

    3. 輸入0,期待返回0

    4. 輸入非數值類型,比如None[]{},期待拋出TypeError

  • 可以把上面的測試用例放到一個測試模塊裏,就是一個完整的單元測試;

3.1 編寫單元測試

# 編寫一個Dict類,類的行爲和dict一致,可以通過屬性來訪問:
>>> d = Dict(a=1, b=2)
>>> d['a']
1
>>> d.a
1

# 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
import unittest

from mydict import Dict

class TestDict(unittest.TestCase):   # 編寫一個測試類,從unittest.TestCase繼承

# 以test開頭的方法就是測試方法,否則不是,在測試的時候不會被執行

# 對每一類測試都需要編寫一個test.xxx()方法,unittest.TestCase提供了很多內置的條件判斷
# 只需要調用這些方法就可以斷言輸出是否是我們所期望的,最常用的斷言是assertEqual()

    def test_init(self):
        d = Dict(a = 1, b = 'test')
        self.assertEqual(d.a, 1)  # self.assertEqual(abs(-1), 1) # 斷言函數返回的結果與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):  # 此斷言是期待拋出指定類型的Error
            value = d['empty']   # 通過d['empty']訪問不存在的key時,斷言會拋出KeyError

    def test_attrerror(self):
        d = Dict()
        with self.assertRaises(AttributeError):
            value = d.empty   # 通過d.empty訪問不存在的key時,拋出AttributError

3.2 運行單元測試

  • 19_mydict_test.py的最後加上:
if __name__ = '__main__'
	unittest.main()
  • 運行
# 把 19_mydict_test.py 當做正常的python腳本運行:
E:\4_Programe\1_Python\3_Code\1_LiaoDaDa>python 20_mydict_test.py
.....
----------------------------------------------------------------------
Ran 5 tests in 0.001s

OK

# 或者在命令行通過參數 -m unittest 直接運行單元測試:
E:\4_Programe\1_Python\3_Code\1_LiaoDaDa>python -m unittest 19_mydict_test
.....
----------------------------------------------------------------------
Ran 5 tests in 0.001s

OK

3.3 setUptearDown

  • 可以在單元測試中編寫兩個特殊的setUp()tearDown()方法。這兩個方法會分別在每調用一個測試方法的前後分別被執行。
class TestDict(unittest.TestCase):

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

    def tearDown(self):
        print('tearDown...')
  • 運行結果:
E:\4_Programe\1_Python\3_Code\1_LiaoDaDa>python -m unittest 19_mydict_test
setUp...
tearDown...
.setUp...
tearDown...
.setUp...
tearDown...
.setUp...
tearDown...
.setUp...
tearDown...
.
----------------------------------------------------------------------
Ran 5 tests in 0.001s

OK

3.4 EX

  • 對Student類編寫單元測試,結果發現測試不通過,請修改Student類,讓測試通過:
# -*- coding: utf-8 -*-
import unittest

class Student(object):
    def __init__(self, name, score):
        self.name = name
        self.score = score
    def get_grade(self):
        if self.score >= 60:
            return 'B'
        if self.score >= 80:
            return 'A'
        return 'C'
  • 進行如下修改:
# -*- coding: utf-8 -*-
import unittest

class Student(object):
    def __init__(self, name, score):
        self.name = name
        self.score = score
    def get_grade(self):
        if 80 <= self.score <= 100:
            return 'A'
        elif 60 <= self.score <= 80:
            return 'B'
        elif 0 <= self.score < 60:
            return 'C'
        else:
            raise ValueError
class TestStudent(unittest.TestCase):

    def test_80_to_100(self):
        s1 = Student('Bart', 80)
        s2 = Student('Lisa', 100)
        self.assertEqual(s1.get_grade(), 'A')
        self.assertEqual(s2.get_grade(), 'A')

    def test_60_to_80(self):
        s1 = Student('Bart', 60)
        s2 = Student('Lisa', 79)
        self.assertEqual(s1.get_grade(), 'B')
        self.assertEqual(s2.get_grade(), 'B')

    def test_0_to_60(self):
        s1 = Student('Bart', 0)
        s2 = Student('Lisa', 59)
        self.assertEqual(s1.get_grade(), 'C')
        self.assertEqual(s2.get_grade(), 'C')

    def test_invalid(self):
        s1 = Student('Bart', -1)
        s2 = Student('Lisa', 101)
        with self.assertRaises(ValueError):
            s1.get_grade()
        with self.assertRaises(ValueError):
            s2.get_grade()

if __name__ == '__main__':
    unittest.main()
  • 運行結果:
.... 
---------------------------------------------------------------------- 
Ran 4 tests in 0.000s 

OK

4. 文檔測試

4.1 doctest

  • python官方文檔中都有很多示例代碼,這些代碼和其他說明可以寫在註釋中,並且可以直接粘貼出來運行,那可不可以自動執行寫在註釋中的這些代碼呢?
  • 使用doctest模塊可以直接提取註釋中的代碼並執行測試:
# 使用doctest來測試上次編寫的Dict類
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()
  • 在命令行運行python 20_doctest.py,發現什麼輸出都沒有,這說明我們編寫的doctest運行是正確的。如果把__getattr__方法註釋掉,再次運行:
E:\4_Programe\1_Python\3_Code\1_LiaoDaDa> python 20_doctest.py
**********************************************************************
File "20_doctest.py", line 7, in __main__.Dict
Failed example:
    d1.x
Exception raised:
    Traceback (most recent call last):
      File "E:\1_Install_Total\8_Anaconda\lib\doctest.py", line 1329, in __run
        compileflags, 1), test.globs)
      File "<doctest __main__.Dict[2]>", line 1, in <module>
        d1.x
    AttributeError: 'Dict' object has no attribute 'x'
**********************************************************************
File "20_doctest.py", line 13, in __main__.Dict
Failed example:
    d2.c
Exception raised:
    Traceback (most recent call last):
      File "E:\1_Install_Total\8_Anaconda\lib\doctest.py", line 1329, in __run
        compileflags, 1), test.globs)
      File "<doctest __main__.Dict[6]>", line 1, in <module>
        d2.c
    AttributeError: 'Dict' object has no attribute 'c'
**********************************************************************
1 items had failures:
   2 of   9 in __main__.Dict
***Test Failed*** 2 failures.
  • 注意到最後3行代碼。當模塊正常導入時,doctest不會被執行。只有在命令行直接運行時,才執行doctest。所以,不必擔心doctest會在非測試環境下執行。

4.2 EX

  • 對函數fact(n)編寫doctest並執行:
def fact(n):
    '''
    Calculate 1*2*...*n

    >>> fact(1)
    1
    >>> fact(10)
    ?
    >>> fact(-1)
    ?
    '''
    if n < 1:
        raise ValueError()
    if n == 1:
        return 1
    return n * fact(n - 1)
if __name__ == '__main__':
    import doctest
    doctest.testmod()
  • 修改之後:
def fact(n):
    '''
    Calculate 1*2*...*n

    >>> fact(1)
    1
    >>> fact(10)
    3628800
    >>> fact(-1)
    Traceback (most recent call last):
        ...
    ValueError: n must be >= 1
    '''
    if n < 1:
        raise ValueError()
    if n == 1:
        return 1
    return n * fact(n - 1)
    
if __name__ == '__main__':
    import doctest
    doctest.testmod()

# 運行結果
********************************************************************** 
File "C:\Users\Jack_ZD\AppData\Local\Temp\learn_python_zaw1p1zl_py\test_1.py", line 10, in __main__.fact 
Failed example: 
    fact(-1) 
Expected: 
    Traceback (most recent call last): 
        ... 
    ValueError: n must be >= 1 
Got: 
    Traceback (most recent call last): 
      File "E:\1_Install_Total\8_Anaconda\lib\doctest.py", line 1329, in __run 
        compileflags, 1), test.globs) 
      File "<doctest __main__.fact[2]>", line 1, in <module> 
        fact(-1) 
      File "C:\Users\Jack_ZD\AppData\Local\Temp\learn_python_zaw1p1zl_py\test_1.py", line 16, in fact 
        raise ValueError() 
    ValueError 
********************************************************************** 
1 items had failures: 
   1 of   3 in __main__.fact 
***Test Failed*** 1 failures.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章