Python复习笔记3——测试与调试技巧
一、错误处理
1.1 try
Python内置了一套try...except...finally...
的错误处理机制。
当我们认为某些代码可能会出错时,就可以用try
来运行这段代码,如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except
语句块,执行完except
后,如果有finally
语句块,则执行finally
语句块,至此,执行完毕。
1 | try: |
上面的代码在计算10 / 0
时会产生一个除法运算错误:
1 | try... |
错误应该有很多种类,如果发生了不同类型的错误,应该由不同的except
语句块处理。
此外,如果没有错误发生,可以在except
语句块后面加一个else
,当没有错误发生时,会自动执行else
语句
常见的错误类型和继承关系参考:https://docs.python.org/3/library/exceptions.html#exception-hierarchy
1.2 记录错误
Python内置的logging
模块可以非常容易地记录错误信息打印出来,然后分析错误原因,同时,让程序继续执行下去:
1 | import logging |
同样是出错,但程序打印完错误信息后会继续执行
1 | ERROR:root:division by zero |
1.3 抛出错误
1 | def foo(s): |
在bar()
函数中,捕获了错误打印一个ValueError!
后,又把错误通过raise
语句抛出去了。这种处理方式较为常见。捕获错误目的只是记录一下,便于后续追踪。但是,由于当前函数不知道应该怎么处理该错误,所以,最恰当的方式是继续往上抛,让顶层调用者去处理。
raise
语句如果不带参数,就会把当前错误原样抛出。
二、调试
众所周知,程序能一次写完并正常运行的概率几乎不存在。总会有各种各样的bug,因此,需要一整套调试程序的手段来修复bug。
2.1 print
最简单粗暴的方法就是用print()
把可能有问题的变量打印出来看看,用print()
最大的坏处是将来还得删掉它。
2.2 断言assert
凡是用print()
来辅助查看的地方,都可以用断言(assert)来替代:assert 表达式, '打印的信息'
,如果断言assert表达式为True,则无效果继续执行,如果断言表达式出错,则打印信息。
1 | def foo(s): |
assert
的意思是,表达式n != 0
应该是True
,否则打印信息。
如果断言失败,assert
语句本身就会抛出AssertionError
,这样程序中如果到处充斥着assert
,和print()
相比也好不到哪去。不过,启动Python解释器时可以用-O
参数来关闭assert
:
1 | $ python -O err.py |
2.3 logging
和assert
比,logging
不会抛出错误,而且可以输出到文件:
1 | import logging |
它允许你指定记录信息的级别,有debug
,info
,warning
,error
等几个级别,当我们指定level=INFO
时,logging.debug
就不起作用了。同理,指定level=WARNING
后,debug
和info
就不起作用了。这样一来,你可以放心地输出不同级别的信息,也不用删除,最后统一控制输出哪个级别的信息。
logging
的另一个好处是通过简单的配置,一条语句可以同时输出到不同的地方,比如console和文件。
2.4 单步调试与断点
(1)pdb
启动Python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态。
1 | $ 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
开头的方法不被认为是测试方法,测试的时候不会被执行。
1 | import unittest |
对每一类测试都需要编写一个test_xxx()
方法。由于unittest.TestCase
提供了很多内置的条件判断,我们只需要调用这些方法就可以断言输出是否是我们所期望的。最常用的断言就是assertEqual()
:
1 | self.assertEqual(abs(-1), 1) # 断言函数返回的结果与1相等 |
(2)运行单元测试
最简单的运行方式是:
1 | if __name__ == '__main__': |
另一种方法是在命令行通过参数-m unittest
直接运行单元测试:
1 | $ python -m unittest mydict_test |
3.2 文档测试
Python内置的“文档测试”(doctest)模块可以直接提取注释中的代码并执行测试。
当我们编写注释时,如果写上这样的注释:
1 | def fact(n): |
无疑更明确地告诉函数的调用者该函数的期望输入和输出。
使用doctest进行测试:
1 | if __name__=='__main__': |
运行测试程序:
1 | $ python test.py |
什么输出也没有。这说明我们编写的doctest运行都是正确的。