文章目錄
- 錯誤:
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
可以指定記錄信息的級別,有debug
,info
,warning
,error
幾個級別,從低到高,當我們指定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
、1.2
、0.99
,期待返回值與輸入相同; -
輸入負數,比如
-1
、-1.2
、-0.99
,期待返回值與輸入相反; -
輸入
0
,期待返回0
; -
輸入非數值類型,比如
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 setUp
與tearDown
- 可以在單元測試中編寫兩個特殊的
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.