8.python错误和调试
在程序运行过程中,总会遇到各种各样的错误。有代码逻辑错误,有运行时错误,有用户输入错误等。
此外,还可以通过使用pdb进行调试。
错误处理
try
python提供了 try...except...finally... 来处理异常。
不同的错误类型,可以由不同的except来处理。 finally 可以没有。
如果没有错误发生,可以在except语句块后面加一个else,当没有错误发生时,会自动执行else语句
错误其实也是class,所有的错误类型都继承自BaseException,所以在使用except时需要注意的是,捕获父类之后,下面捕获子类的代码就不会执行到了。 和java类似。
python常见的错误类型和继承关系 exception-hierarchy
示例如下
1 | def error_test(num): |
调用栈
如下面错误的堆栈信息
1 | Traceback (most recent call last): |
Traceback (most recent call last):
第一行,告诉我们这是错误的跟踪信息。 后面可以逐行分析, 最终找到错误的根源。
出错的时候,一定要分析错误的调用栈信息,才能定位错误的位置。
记录错误
logging 模块可以记录错误信息。
捕获错误,然后记录错误堆栈信息,让程序继续往下运行。
抛出错误
因为错误是class,捕获一个错误就是捕获到该class的一个实例。因此,错误并不是凭空产生的,而是有意创建并抛出的。Python的内置函数会抛出很多类型的错误,我们自己编写的函数也可以抛出错误。
只有在必要的时候才定义我们自己的错误类型。如果可以选择Python已有的内置的错误类型(比如ValueError,TypeError),尽量使用Python内置的错误类型。
我们还可以捕获异常,记录之后将异常重新抛出,也可以转换为其他异常(要符合逻辑,不能随意转换)
1 | def bar(): |
程序也可以主动抛出错误,让调用者来处理相应的错误。但是,应该在文档中写清楚可能会抛出哪些错误,以及错误产生的原因。
调试
使用print()将相关变量的值打印出来,但是将来还需要删除它。
断言
凡是用print()来辅助查看的地方,都可以用断言(assert)来替代:
assert n != 0, ‘n is zero!’
assert的意思是,表达式n != 0应该是True,否则,根据程序运行的逻辑,后面的代码肯定会出错。
如果断言失败,assert语句本身就会抛出AssertionError
启动Python解释器时可以用-O(大写的英文O)参数来关闭assert, 关闭后 assert 可以当做 pass
logging
把print()替换为logging是第3种方式,和assert比,logging不会抛出错误,而且可以输出到文件
1 | import logging |
logging 可以控制日志的级别, 有debug,info,warning,error, 也可以通过配置,将日志输出到不同的地方。
pdb
第4种方式是启动Python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态。
以参数-m pdb 启动后,pdb定位到下一步要执行的代码,可以输入命令 l(小写的L) 来查看代码。输入命令n可以单步的执行代码。
可以输入p 变量名来查看变量。输入命令q 来结束调试。
pdb.set_trace()
pdb 单步执行太麻烦,我们只需要import pdb,然后,在可能出错的地方放一个pdb.set_trace(),就可以设置一个断点:
1 | # err.py |
运行代码,程序会自动在pdb.set_trace()暂停并进入pdb调试环境,可以用命令p查看变量,或者用命令c继续运行
IDE调试
VS Code 插件。
虽然用IDE调试起来比较方便,但是最后你会发现,logging才是终极武器。
单元测试
单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。
单元测试通过后,如果对代码做了修改,可以重新跑一遍单元测试,看是否通过,来判断修改是否对原来的逻辑造成了影响。
编写单元测试时,我们需要编写一个测试类,从unittest.TestCase继承
以test开头的方法就是测试方法,不以test开头的方法不被认为是测试方法,测试的时候不会被执行。
对每一类测试都需要编写一个test_xxx()方法。由于unittest.TestCase提供了很多内置的条件判断,最常用的断言就是assertEqual()
1 | self.assertEqual(abs(-1), 1) # 断言函数返回的结果与1相等 |
另一种重要的断言就是期待抛出指定类型的Error
运行单元测试
最简单的运行方式是在mydict_test.py的最后加上两行代码,这样就可以把文件当做正常脚本运行。
1 | if __name__ == '__main__': |
另一种方法是在命令行通过参数-m unittest直接运行单元测试。
这是推荐的做法,因为这样可以一次批量运行很多单元测试,并且,有很多工具可以自动来运行这些单元测试
setUp与tearDown
可以在单元测试中编写两个特殊的setUp()和tearDown()方法。这两个方法会分别在每调用一个测试方法的前后分别被执行。
setUp()和tearDown()方法有什么用呢?设想你的测试需要启动一个数据库,这时,就可以在setUp()方法中连接数据库,在tearDown()方法中关闭数据库,这样,不必在每个测试方法中重复相同的代码
类似 java单元测试中的 @Before 和 @After
文档测试
Python官方文档中的示例代码,比如 re模块 就带了很多示例代码:
1 | >> import re |
可以把这些示例代码在Python的交互式环境下输入并执行,结果与文档中的示例代码显示的一致。
这些代码与其他说明可以写在注释中,然后,由一些工具来自动生成文档。
既然这些代码本身就可以粘贴出来直接运行,也可以自动执行写在注释中的这些代码。
当我们编写注释时,如果写上这样的注释
1 |
|
可以明确地告诉函数的调用者该函数的期望输入和输出
并且,Python内置的“文档测试”(doctest)模块可以直接提取注释中的代码并执行测试。
doctest严格按照Python交互式命令行的输入和输出来判断测试结果是否正确。只有测试异常的时候,可以用…表示中间一大段烦人的输出。
示例
1 |
|
doctest不仅可以用来测试,还可以直接作为示例代码,通过某些文档生成工具,可以自动把包含doctest的注释提取出来。