错误、调试和测试
错误处理
在程序运行过程中,可以实现设定发生错误时返回一个错误代码,这样就可以知道是否出错及其出错原因是什么。
错误处理机制python的错误处理机制:try...except...finally..
。
当认为某段代码可能会出错时,用try
来运行这段代码,如果执行出错,则后续的代码不会继续执行,而是直接跳转到错误处理代码except
语句块中,执行完except
语句块后,如果还有finally
语句块,则执行语句块,错误处理机制执行完毕。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 try : print ('try...' ) r=10 /int ('a' ) print ('result:' ,r) except ValueError as e: print ('ValueError:' ,e) except ZeroDivisionError as e: print ('except:' ,e) else : print ('no error!' ) finally : print ('finally...' ) print ('END' )try ...ValueError: invalid literal for int () with base 10 : 'a' finally ...END
如果没有错误发生,except
语句块不会被执行,但如果有finally
语句块,finally
语句块一定会被执行。在使用except
时要注意先写子类的异常 ,基类的异常在后。
使用try...except
捕获错误可以跨越多层调用。不需要在每个可能出错的地方去捕获错误。
调用栈
若错误没有被捕获,就会一直往上抛,直至最后被python解释器捕获,打印一个错误信息,程序就退出了。出错的时候一定要分析错误的调用栈信息,才能定位错误的位置。
记录错误
如果能捕获错误,就可以把错误堆栈打印出来,分析错误原因的同时,让程序继续执行下去。
logging
模块可以记录错误信息。导包后的调用形式:logging.exception(e)
。
抛出错误
因为错误是class,捕获一个错误就是捕获到该class的一个实例。因此自己编写的函数也可以抛出错误。若需要抛出错误,可以先定义一个错误的class,然后用raise
语句抛出一个错误。但只有在必要的时候才定义自己的错误类型,若可以选择内置的错误类型,尽量使用内置的错误类型。
抛出错误例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class FooError (ValueError ): pass def foo (s ): n=int (s) if n==0 : raise FooError('invalid value: %s' % s) return 10 /n foo('0' ) Traceback (most recent call last): File "main.py" , line 8 , in <module> foo('0' ) File "main.py" , line 6 , in foo raise FooError('invalid value: %s' % s) __main__.FooError: invalid value: 0
另一种错误处理方式为,捕获错误后,又把错误通过raise
语句往上抛,让顶层的调用者去处理。
raise
语句如果不带参数,就会把当前错误原样抛出。若在except
中raise
一个Error,可以将一种类型的错误转化为另一种类型,但不能将一个IOError
转换成毫不相干的ValueError
。
练习题
运行下面的代码,根据异常信息进行分析,定位出错误源头,并修复:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from functools import reducedef str2num (s ): return int (s) def calc (exp ): ss = exp.split('+' ) ns = map (str2num, ss) return reduce(lambda acc, x: acc + x, ns) def main (): r = calc('100 + 200 + 345' ) print ('100 + 200 + 345 =' , r) r = calc('99 + 88 + 7.6' ) print ('99 + 88 + 7.6 =' , r) main()
修复后的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from functools import reducedef str2num (s ): return float (s) def calc (exp ): ss = exp.split('+' ) ns = map (str2num, ss) return reduce(lambda acc, x: acc + x, ns) def main (): r = calc('100 + 200 + 345' ) print ('100 + 200 + 345 =' , r) r = calc('99 + 88 + 7.6' ) print ('99 + 88 + 7.6 =' , r) main() 100 + 200 + 345 = 645.0 99 + 88 + 7.6 = 194.6
调试
方法一:print()
用print()
把可能有问题的变量打印出来看
方法二:断言(assert)
凡是用print()来看的地方,都可以用断言(assert)来替代。如:
1 2 3 4 5 6 7 8 9 10 11 12 def foo (s ): n=int (s) assert n!=0 ,'n is zero!' return 10 /n foo('0' ) Traceback (most recent call last): File "main.py" , line 5 , in <module> foo('0' ) File "main.py" , line 3 , in foo assert n!=0 ,'n is zero!' AssertionError: n is zero!
python解释器中可以用python -O 文件名
来关闭assert。关闭后,所有的assert语句都相当于一个pass。
方法三:logging
把print()
替换为logging
后,与assert
比,logging
不会抛出错误,且可以输出到文件。通过配置logging,可以将一条语句同时输出到不同的地方,如文件和控制台。
1 2 3 4 5 6 7 8 9 10 11 12 13 import logging logging.basicConfig(level=logging.INFO) s='0' n=int (s) logging.info('n=%d' % n) print (10 /n)INFO:root:n=0 Traceback (most recent call last): File "main.py" , line 7 , in <module> print (10 /n) ZeroDivisionError: division by zero
logging有debug、info、warning、error等几个级别,当指定level=INFO
时,logging.debug
就不起作用了。当指定level=WARNING
时,logging.debug
和logging.INFO
就不起作用了。
方法四:pdb
启动python的调制器pdb,可以让程序以单步方式运行,类似于c++的单点调试,可以随时查看代码的运行状态。pdb需要在控制台里输入语句,其形式为python -m pdb 文件名
。
示例如下:
pdb内的命令用法如下:
单步执行代码:输入 n
查看变量:p 变量名
退出程序:输入 q
方法五:pdb.set_trace()
该方法也是用pdb,需要在控制台里输入语句,但不需要单步执行。类似于c++的断点调试,只需要import pdb,然后在可能出错的地方一个pdb.set_trace()
,就可以设置一个断点。
示例如下:
进入pdb调试环境后,可用p 变量名
查看变量,输入c
继续运行。
方法六:IDE
高效率的设置断点、单步执行,需要一个支持调试功能的IDE。目前常用的Python IDE有:
Visual Studio Code、PyCharm、加上pydev插件的Eclipse。
单元测试
单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。将针对某函数写的测试用例放到一个测试模块里,便是一个完整的单元测试。
若单元测试通过,则说明所测试的函数能够正常工作;若单元测试不通过,要么函数有bug,要么测试条件输入不正确。
单元测试需要引用unittest
模块 。常调用模块内置的条件判断来断言输出是否是期望值,最常用的断言语句 是:self.assertEqual(函数名(参数),期待值)
。
另一种重要的断言是期待抛出指定类型的Error,用法是:
1 2 with self.assertRaises(指定类型的Error): value=函数.属性或者是函数(参数)
运行单元测试
方法一: 在代码最后加上两行代码,直接运行程序
1 2 if __name__=='__main__' : unittest.main()
方法二: 在命令行 通过参数python -m unittest 文件名
运行单元测试。
setUp
与tearDown
每调用一个测试方法前被执行:setUp()
在测试方法执行完后被执行:tearDown()
练习题
对Student类编写单元测试,结果发现测试不通过,请修改Student类,让测试通过:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import unittestclass Student (object ): def __init__ (self, name, score ): self.name = name self.score = score def get_grade (self ): if self.score >= 60 : return 'B' if self.score >= 80 : return 'A' return 'C' 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()
修改后的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import unittestclass Student (object ): def __init__ (self, name, score ): self.name = name self.score = score def get_grade (self ): if self.score >= 60 and self.score < 80 : return 'B' elif self.score >= 80 and self.score <= 100 : return 'A' elif self.score >= 0 and self.score < 60 : return 'C' else : raise ValueError() 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() .... ---------------------------------------------------------------------- Ran 4 tests in 0.001 s OK
文档测试
文档测试(doctest
)模块可以自动提取写在注释中的代码并执行测试。
dectest
严格按照命令行的输入和输出来判断测试结果是否正确。若运行文档测试后没有输出,则说明程序是正确的,在测试异常时,可以用...
表示中间的出错的信息。
运行文档测试,需要先在代码的最后加上一下三行代码 ,然后在命令行中通过python 文件名
来执行文档测试:
1 2 3 if __name__=='__main__' : import doctest doctest.testmod()
在模块正常导入时不会执行文档测试,只有在命令行直接运行时才执行文档测试。
练习题
对函数fact(n)
编写doctest并执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def fact (n ): ''' Calculate 1*2*...*n >>> fact(1) 1 >>> fact(10) ? >>> fact(-1) ? ''' if n < 1 : raise ValueError() if n == 1 : return 1 return n * fact(n - 1 ) if __name__ == '__main__' : import doctest doctest.testmod()
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 ) if __name__ == '__main__' : import doctest doctest.testmod()