錯誤處理
和Java類似,Python提供了一套錯誤處理機制,語法是 try...except...finally...
。
可以將你認爲會發生錯誤的代碼用try
包裹起來並用except
捕獲指定的錯誤或異常,最後使用finally
執行語句塊如發生錯誤後也要進行資源的回收等。
Python的錯誤其實也是class,所有的錯誤類型都繼承自BaseException
。
出錯的時候,一定要分析錯誤的調用棧信息,才能定位錯誤的位置。
可以通過python內置的logging
模塊來記錄異常錯誤信息如logging.exception(e)
,這個logging
模塊後面會有專門的文章來介紹。
我們也可以手動向上層拋出異常,可以通過raise
關鍵字,如拋出屬性找不到的異常raise AttributeError("'Model' object has no attribute '%s'" % key)
。在except
代碼塊裏捕獲到異常後我們也可以通過raise
將異常原封不動地拋出,也可以拋出自定義錯誤。
如下圖,我們可以看出AttributeError
就是最終繼承於BaseException
。
如下面代碼所示。我在main()
方法裏寫了多個except
語句來捕捉各個異常,但是由於捕捉到異常後是從上到下匹配的且Exception
是ZeroDivisionError
的父類,故而except ZeroDivisionError as e:
永遠也執行不了。
捕捉到異常後,我通過logging
模塊來記錄異常錯誤信息並用raise
將錯誤信息原樣拋出給上層。當然是否需要拋出看當時業務需求。
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:
print('Error:', e)
# 用logging模塊記錄異常
logging.exception(e)
raise # 原樣將捕捉到的異常拋出
# raise ZeroDivisionError("除數爲0發生的錯誤") # 拋出指定異常
except ZeroDivisionError as e:
pass # 注意這裏是永遠執行不到的,因爲Exception是ZeroDivisionError的父類,except捕捉到異常後從上往下匹配異常
finally:
print('finally...')
main()
運行結果如下圖所示
調試
最簡單粗暴的方式就是使用print()
函數打印需要查看的變量的值。
但是這種方式僅限於平時學習使用。在工作當中是絕對不允許的。
斷言assert
這個和Java裏的斷言非常類似。都是用於判斷一個表達式,在表達式條件爲 false 的時候觸發異常。
語法是 assert expression [, arguments]
,等價於如下代碼。當然參數是可選的,不過一般最好添加參數,可以給人提示錯誤信息。
if not expression:
raise AssertionError(arguments)
如下面計算一個數的平方,用assert
判斷必須是數字,否則就會拋出異常
# 計算一個數的平方
def square(num):
assert isinstance(num, (int,float)), "{} 不是數字類型".format(num)
print("the square of {} is {}".format(num, num**2))
square(10)
square("10")
運行結果如下
當然如果程序中導出都充斥着assert
也非常不好,一般斷言都是在測試代碼功能時發揮作用的。
不過我們可以在啓動python解釋器時通過參數-O
來關閉assert
,此時assert
語句可以看做pass
。
如下所示。關閉assert
後異常信息就不同了。注意參數是大寫的字母O不是數字零。
logging
必須配置logging.basicConfig(level=logging.INFO)
設定日誌級別,否則會信息是無法打印的。
更加詳細的配置信息後續再寫博客介紹。
import logging
logging.basicConfig(level=logging.INFO)
logging.info("測試中logging模塊")
pdb
啓動Python的調試器pdb(啓動python解釋器時添加參數-m pdb
),讓程序以單步方式運行,可以隨時查看運行狀態。
我們也可以在代碼中導入pdb
並在需要調試的地方寫上pdb.set_trace()
,這樣就可以跳躍進行調試。
輸入 l
查看代碼。字母L的小寫。
輸入 n
執行下一行代碼。單步執行。
輸入 p 變量名
查看變量。
輸入 c
繼續運行到下一個設置pdb.set_trace()
的代碼。
輸入 q
退出。
# -*- coding: UTF-8 -*-
import pdb
import logging
logging.basicConfig(level=logging.INFO)
logging.info("測試中logging模塊")
# 計算一個數的平方
def square(num):
assert isinstance(num, (int,float)), "{} 不是數字類型".format(num)
print("the square of {} is {}".format(num, num**2))
return num**2
pdb.set_trace()
res = square(10)
print(1+2)
pdb.set_trace()
print(3+4)
logging.info("finish....")
運行結果如下
IDE
當然調試最好的方式還是IDE編輯器。如PyCharm
等。
不過也最好結合logging
去調試。
單元測試
爲了測試我們的代碼功能是否正常,可以寫一組測試用例放在一個模塊,這就叫做一個完整的單元測試。
當後續我們的代碼修改後,依舊可以運行這個單元測試保證我們的代碼在功能上是正常的。
python內置模塊unittest
可以讓我們去寫單元測試。
首先我們需要編寫一個測試類,該測試類需要繼承unittest.TestCase
。該測試類下以test
開頭的方法就是測試方法,沒有以test
開頭的方法就不是測試方法,在測試的時候不會被執行。
unittest.TestCase
類提供了很多內置的條件判斷,最常用的就是assertEqual
、assertTrue
以及發生異常時with self.assertRaises(AttributeError):
有兩種方式運行單元測試.
- 在測試代碼裏添加
unittest.main()
,然後像運行普通python腳本一樣運行該單元測試文件。 - 命令行輸入
python -m unittest 文件名
來執行單元測試。這種方式更加推薦常用,因爲這樣可以一次批量運行很多單元測試。
下面是我的mydict.py
文件,自定義一個字典類
# -*- coding: UTF-8 -*-
class Dict(dict):
def __init__(self, **kw):
super().__init__(**kw)
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError("'Dict' object has no attribute '%s'" % key)
def __setattr__(self, key, value):
self[key] = value
下面是我的單元測試文件mydict_test.py
# -*- coding: UTF-8 -*-
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.empt
def abc(self):
10/0
def test_abc(self):
self.assertEqual(1+2, 3)
# 在用python -m unittest 文件名 執行單元測試時不需要下面兩行代碼
if __name__ == '__main__':
unittest.main()
採用第一種方式運行單元測試結果如下圖所示
採用第二種方式運行單元測試結果如下圖所示
setUp與tearDown
可以在單元測試中編寫兩個特殊的setUp()
和tearDown()
方法。這兩個方法會分別在每調用一個測試方法的前後分別被執行。
設想你的測試需要啓動一個數據庫,這時,就可以在setUp()
方法中連接數據庫,在tearDown()
方法中關閉數據庫,這樣,不必在每個測試方法中重複相同的代碼。
class TestDict(unittest.TestCase):
def setUp(self):
print("setUp ... ")
def tearDown(self):
print("tearDown ... ")
單元測試小結
單元測試可以有效地測試某個程序模塊的行爲,是未來重構代碼的信心保證。
單元測試的測試用例要覆蓋常用的輸入組合、邊界條件和異常。
單元測試代碼要非常簡單,如果測試代碼太複雜,那麼測試代碼本身就可能有bug。
單元測試通過了並不意味着程序就沒有bug了,但是不通過程序肯定有bug。
文檔測試
函數或者模塊裏的文檔註釋有一些樣例代碼,文檔測試
就可以將這些樣例代碼抽取出來放在交互式的python環境下運行看是否和預期結果一致。
如果是遇到異常的話應該在文檔註釋裏這麼寫,其中用...
表示一大串異常信息。
>>> fact("abc")
Traceback (most recent call last):
...
TypeError: 必須傳入整數 參數是abc
下面代碼就展示瞭如何用文檔測試
測試自己寫的fact
方法的正確性。
如何運行文檔測試呢?可以參考下面代碼的最後三行,導入了doctest
模塊。
# -*- coding: UTF-8 -*-
def fact(n):
'''
計算n對應的階乘
>>> fact(1)
1
>>> fact(5)
120
>>> fact("abc")
Traceback (most recent call last):
...
TypeError: 必須傳入整數 參數是abc
>>> fact(-1)
Traceback (most recent call last):
...
ValueError: 不能小於1
>>>
:param
:return
'''
if not isinstance(n, int):
raise TypeError("必須傳入整數 參數是{}".format(n))
if n < 1:
raise ValueError("不能小於1")
if n == 1:
return 1
return n * fact(n-1)
if __name__=='__main__':
import doctest
doctest.testmod()
在pycharm中鼠標點擊到文檔測試代碼部分然後右鍵即可運行文檔測試。
在命令行中輸入python test.py
即可運行文檔測試,如果什麼信息都沒有就表示文檔測試通過。如果出錯了就應該有信息(下面的截圖中我故意寫錯來展示錯誤信息)