一、错误处理
1.1 try
Python内置了一套try...except...finally...
的错误处理机制。
当我们认为某些代码可能会出错时,就可以用try
来运行这段代码,如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except
语句块,执行完except
后,如果有finally
语句块,则执行finally
语句块,至此,执行完毕。
try:
print('try...')
r = 10 / 0
print('result:', r)
except ZeroDivisionError as e:
print('except:', e)
finally:
print('finally...')
print('END')
上面的代码在计算10 / 0
时会产生一个除法运算错误:
try...
except: division by zero
finally...
END
错误应该有很多种类,如果发生了不同类型的错误,应该由不同的except
语句块处理。
此外,如果没有错误发生,可以在except
语句块后面加一个else
,当没有错误发生时,会自动执行else
语句
常见的错误类型和继承关系参考:https://docs.python.org/3/library/exceptions.html#exception-hierarchy
1.2 记录错误
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')
同样是出错,但程序打印完错误信息后会继续执行
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
1.3 抛出错误
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
语句抛出去了。这种处理方式较为常见。捕获错误目的只是记录一下,便于后续追踪。但是,由于当前函数不知道应该怎么处理该错误,所以,最恰当的方式是继续往上抛,让顶层调用者去处理。
raise
语句如果不带参数,就会把当前错误原样抛出。
二、调试
众所周知,程序能一次写完并正常运行的概率几乎不存在。总会有各种各样的bug,因此,需要一整套调试程序的手段来修复bug。
2.1 print
最简单粗暴的方法就是用print()
把可能有问题的变量打印出来看看,用print()
最大的坏处是将来还得删掉它。
2.2 断言assert
凡是用print()
来辅助查看的地方,都可以用断言(assert)来替代:assert 表达式, '打印的信息'
,如果断言assert表达式为True,则无效果继续执行,如果断言表达式出错,则打印信息。
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
,这样程序中如果到处充斥着assert
,和print()
相比也好不到哪去。不过,启动Python解释器时可以用-O
参数来关闭assert
:
$ python -O err.py
2.3 logging
和assert
比,logging
不会抛出错误,而且可以输出到文件:
import logging
logging.basicConfig(level=logging.INFO)
s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10 / n)
它允许你指定记录信息的级别,有debug
,info
,warning
,error
等几个级别,当我们指定level=INFO
时,logging.debug
就不起作用了。同理,指定level=WARNING
后,debug
和info
就不起作用了。这样一来,你可以放心地输出不同级别的信息,也不用删除,最后统一控制输出哪个级别的信息。
logging
的另一个好处是通过简单的配置,一条语句可以同时输出到不同的地方,比如console和文件。
2.4 单步调试与断点
(1)pdb
启动Python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态。
$ python -m pdb err.py
以参数-m pdb
启动后,pdb定位到下一步要执行的代码。
- 输入命令
l
来查看代码 - 输入命令
n
可以单步执行代码。 - 输入命令
p 变量名
来查看变量 - 输入命令
q
结束调试,退出程序
(2)pdb.set_trace()
这个方法也是用pdb,但是不需要单步执行,我们只需要import pdb
,然后,在可能出错的地方放一个pdb.set_trace()
,就可以设置一个断点。
运行代码,程序会自动在pdb.set_trace()
暂停并进入pdb调试环境,可以用命令p
查看变量,或者用命令c
继续运行。
(3)IDE
使用一个支持调试功能的IDE,可以更好的设置断点、单步执行。
常规使用推荐Visual Studio Code:https://code.visualstudio.com/,需要安装Python插件。
大型项目推荐PyCharm:http://www.jetbrains.com/pycharm/。
三、测试
3.1 单元测试
单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。
比如对函数abs()
,我们可以编写出以下几个测试用例:
- 输入正数,比如
1
、1.2
、0.99
,期待返回值与输入相同; - 输入负数,比如
-1
、-1.2
、-0.99
,期待返回值与输入相反; - 输入
0
,期待返回0
; - 输入非数值类型,比如
None
、[]
、{}
,期待抛出TypeError
。
把上面的测试用例放到一个测试模块里,就是一个完整的单元测试。
(1)编写单元测试
编写单元测试时,我们需要编写一个测试类,从unittest.TestCase
继承。
以test
开头的方法就是测试方法,不以test
开头的方法不被认为是测试方法,测试的时候不会被执行。
import unittest
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()
对每一类测试都需要编写一个test_xxx()
方法。由于unittest.TestCase
提供了很多内置的条件判断,我们只需要调用这些方法就可以断言输出是否是我们所期望的。最常用的断言就是assertEqual()
:
self.assertEqual(abs(-1), 1) # 断言函数返回的结果与1相等
(2)运行单元测试
最简单的运行方式是:
if __name__ == '__main__':
unittest.main()
另一种方法是在命令行通过参数-m unittest
直接运行单元测试:
$ python -m unittest mydict_test
3.2 文档测试
Python内置的“文档测试”(doctest)模块可以直接提取注释中的代码并执行测试。
当我们编写注释时,如果写上这样的注释:
def fact(n):
'''
Calculate 1*2*...*n
>>> fact(1)
1
>>> fact(10)
3628800
>>> fact(-1)
Traceback (most recent call last):
...
ValueError
'''
if n < 1:
raise ValueError()
if n == 1:
return 1
return n * fact(n - 1)
无疑更明确地告诉函数的调用者该函数的期望输入和输出。
使用doctest进行测试:
if __name__=='__main__':
import doctest
doctest.testmod()
运行测试程序:
$ python test.py
什么输出也没有。这说明我们编写的doctest运行都是正确的。