Python|学习笔记(九)- 错误、调试和测试

Python学习笔记系列

Posted by Jack on January 10, 2015

前言

本篇是Python学习笔记系列的第(九)篇,主要介绍程序运行过程中的错误、调试和测试。

概述

程序错误大体可以分为两类:

  • Bug,由程序编写问题造成,比如本来应该输出string,结果输出了int
  • 异常,程序运行过程中无法预测的错误,比如写入文件时磁盘满了

Python内置了一套错误处理机制来帮助我们处理错误。

除了做好错误处理,编写良好的测试代码也很重要。

错误处理

  • try

高级语言通常都内置了一套try...except...finally...的错误处理机制,Python也不例外。

当我们认为某些代码可能会出错时,就可以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')

打印如下:

try...
except: division by zero
finally...
END

如果没有错误发生,except语句块不会被执行,但是finally如果有,则一定会被执行(可以没有finally语句)。同时,可以有多个except来捕获不同类型的错误

Python的错误其实也是class,所有的错误类型都继承自BaseException,所以在使用except时需要注意的是,它不但捕获该类型的错误,还把其子类也“一网打尽”

比如:

try:
    foo()
except ValueError as e:
    print('ValueError')
except UnicodeError as e:
    print('UnicodeError')

第二个except永远也捕获不到UnicodeError,因为UnicodeErrorValueError的子类,如果有,也被第一个except给捕获了。

使用try...except捕获错误还有一个巨大的好处,就是可以跨越多层调用,比如函数main()调用foo()foo()调用bar(),结果bar()出错了,这时,只要main()捕获到了,就可以处理。

  • 记录错误

Python内置的logging模块可以非常容易地记录错误信息,通过配置,logging还可以把错误记录到日志文件里,方便事后排查。

  • 抛出错误

如果要抛出错误,首先根据需要,可以定义一个错误的class,选择好继承关系,然后,用raise语句抛出一个错误的实例:

# err_raise.py
class FooError(ValueError):
    pass

def foo(s):
    n = int(s)
    if n==0:
        raise FooError('invalid value: %s' % s)
    return 10 / n

foo('0')

只有在必要的时候才定义我们自己的错误类型。如果可以选择Python已有的内置的错误类型(比如ValueErrorTypeError),尽量使用Python内置的错误类型。

raise语句如果不带参数,就会把当前错误原样抛出。此外,在exceptraise一个Error,还可以把一种类型的错误转化成另一种类型:

try:
    10 / 0
except ZeroDivisionError:
    raise ValueError('input error!')

调试

  • 断言
def foo(s):
    n = int(s)
    assert n != 0, 'n is zero!'
    return 10 / n

def main():
    foo('0')

断言assert的意思是,表达式n != 0应该是True,否则,根据程序运行的逻辑,后面的代码肯定会出错。

  • logging
import logging
logging.basicConfig(level=logging.INFO) # 设置输出等级
s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10 / n)

logging允许你指定记录信息的级别,有debuginfowarningerror等几个级别,当我们指定level=INFO时,logging.debug就不起作用了。同理,指定level=WARNING后,debuginfo就不起作用了。这样一来,你可以放心地输出不同级别的信息,也不用删除,最后统一控制输出哪个级别的信息。

logging的另一个好处是通过简单的配置,一条语句可以同时输出到不同的地方,比如console和文件。

  • pdb

启动Python的调试器pdb,可以让程序以单步方式运行。

python -m pdb err.py
  1. 输入命令l来查看代码

  2. 输入命令n可以单步执行代码

  3. 任何时候都可以输入命令p 变量名来查看变量

  4. 输入命令q结束调试,退出程序。

  • pdb.set_trace()

这个方法也是用pdb,但是不需要单步执行,我们只需要import pdb,然后,在可能出错的地方放一个pdb.set_trace(),就可以设置一个断点。程序会自动在pdb.set_trace()暂停并进入pdb调试环境,可以用命令p查看变量,或者用命令c继续运行。

  • IDE

如果要比较爽地设置断点、单步执行,就需要一个支持调试功能的IDE。

单元测试

单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。

比如对函数abs(),我们可以编写出以下几个测试用例:

1. 输入正数,比如1、1.2、0.99,期待返回值与输入相同;
2. 输入负数,比如-1、-1.2、-0.99,期待返回值与输入相反;
3. 输入0,期待返回0;
4. 输入非数值类型,比如None、[]、{},期待抛出TypeError。

把上面的测试用例放到一个测试模块里,就是一个完整的单元测试。

单元测试通过后有什么意义呢?如果我们对abs()函数代码做了修改,只需要再跑一遍单元测试,如果通过,说明我们的修改不会对abs()函数原有的行为造成影响,如果测试不通过,说明我们的修改与原有行为不一致,要么修改代码,要么修改测试。

编写单元测试时,我们需要编写一个测试类,从unittest.TestCase继承。

test开头的方法就是测试方法,不以test开头的方法不被认为是测试方法,测试的时候不会被执行。

对每一类测试都需要编写一个test_xxx()方法。由于unittest.TestCase提供了很多内置的条件判断,我们只需要调用这些方法就可以断言输出是否是我们所期望的。最常用的断言就是assertEqual()

  • 运行单元测试

一旦编写好单元测试,我们就可以运行单元测试。最简单的运行方式是在mydict_test.py的最后加上两行代码:

if __name__ == '__main__':
    unittest.main()

这样就可以把mydict_test.py当做正常的python脚本运行。

另一种方法是在命令行通过参数-m unittest直接运行单元测试。

  • setUp与tearDown

可以在单元测试中编写两个特殊的setUp()tearDown()方法。这两个方法会分别在每调用一个测试方法的前后分别被执行。

setUp()tearDown()方法有什么用呢?设想你的测试需要启动一个数据库,这时,就可以在setUp()方法中连接数据库,在tearDown()方法中关闭数据库,这样,不必在每个测试方法中重复相同的代码。

文档测试

当我们编写注释时,如果写上这样的注释:

def abs(n):
    '''
    Function to get absolute value of number.
    
    Example:
    
    >>> abs(1)
    1
    >>> abs(-1)
    1
    >>> abs(0)
    0
    '''
    return n if n >= 0 else (-n)

if __name__=='__main__':
    import doctest
    doctest.testmod()

Python内置的“文档测试”(doctest)模块可以直接提取注释中的代码并执行测试。

注意到最后3行代码。当模块正常导入时,doctest不会被执行。只有在命令行直接运行时,才执行doctest。所以,不必担心doctest会在非测试环境下执行。

总结

本章我们学习了如何处理程序编写过程中遇到的错误,以及如何进行程序的调试与测试。下一篇文章我们会讲述基础的I/O编程。