python学习笔记,调试与测试
错误处理
许多高级语言中都内置了一套try...exceot...finally...
的错误处理机制,比如java中的try...catch
,python中也包含一套用于错误处理的代码。
try
和java中的一样,解释器会先执行try
关键字中的代码,如果在某处出错,则会立即停止继续执行try
中的代码段落,而转去执行对应except
中的代码段,执行完后,将跳过else
,如果有finally
,则会执行finally
中的代码。
如果没有发生错误。则解释器会执行完try
、else
和finally
中的所有内容
1 2 3 4 5 6 7 8 9 10 11 12 13 try : print ('try...' ) f = 10 /int (x) print ('result:' , r) except ValueError as e: print ('ValueError:' , e) except ZeroDivisionError as e: print ('ZeroDivisionError:' , e) else : print ('no error!' ) finally : print ('finally...' ) print ('END' )
当x
输入0时,语句f = 10/int(x)
会出现除0错误,此时直接跳转到第7行继续执行,因此最终结果为:
1 2 3 4 try ...ZeroDivisionError: division by zero finally ...END
当x
输入a,或者其他不能转化为数字的字符时,int(x)
就会出现参数错误,此时直接跳转到第5行执行,因此最终结果为:
1 2 3 4 try ...ValueError: invalid literal for int () with base 10 : x finally ...END
但当我们输入正确值时,比如输入2,则不会发生异常,从而得到以下结果:
1 2 3 4 5 try ...result: 5 no error! finally ...END
与java一样,python中的异常也是类,并且有继承关系,所有异常均继承自BaseException
,这一特性导致当我们用某一类型去捕获一个异常时,如果遇到该类型异常的子类异常,也会将其捕获:
1 2 3 4 5 6 try : foo() except ValueError as e: print ('ValueError' ) except UnicodeError as e: print ('UnicodeError' )
由于UnicodeError
是ValueError
类型异常的子类异常,因此第5行之后的代码将永远无法出发,发生UnicodeError
异常时,它将优先被写在前面且可以捕获该异常的第三行except
捕获。
常见异常继承关系可参考python3官方文档:
点击查看异常继承关系
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 53 54 55 56 57 58 59 60 61 62 63 64 BaseException +-- SystemExit +-- KeyboardInterrupt +-- GeneratorExit +-- Exception +-- StopIteration +-- StopAsyncIteration +-- ArithmeticError | +-- FloatingPointError | +-- OverflowError | +-- ZeroDivisionError +-- AssertionError +-- AttributeError +-- BufferError +-- EOFError +-- ImportError | +-- ModuleNotFoundError +-- LookupError | +-- IndexError | +-- KeyError +-- MemoryError +-- NameError | +-- UnboundLocalError +-- OSError | +-- BlockingIOError | +-- ChildProcessError | +-- ConnectionError | | +-- BrokenPipeError | | +-- ConnectionAbortedError | | +-- ConnectionRefusedError | | +-- ConnectionResetError | +-- FileExistsError | +-- FileNotFoundError | +-- InterruptedError | +-- IsADirectoryError | +-- NotADirectoryError | +-- PermissionError | +-- ProcessLookupError | +-- TimeoutError +-- ReferenceError +-- RuntimeError | +-- NotImplementedError | +-- RecursionError +-- SyntaxError | +-- IndentationError | +-- TabError +-- SystemError +-- TypeError +-- ValueError | +-- UnicodeError | +-- UnicodeDecodeError | +-- UnicodeEncodeError | +-- UnicodeTranslateError +-- Warning +-- DeprecationWarning +-- PendingDeprecationWarning +-- RuntimeWarning +-- SyntaxWarning +-- UserWarning +-- FutureWarning +-- ImportWarning +-- UnicodeWarning +-- BytesWarning +-- ResourceWarning
此外,和java一样,python中的异常处理机制也支持多层调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 def foo (s ): return 10 / int (s) def bar (s ): return foo(s) * 2 def main (): try : bar('0' ) except Exception as e: print ('Error:' , e) finally : print ('finally...' )
foo()
函数如果发生异常,在main
函数中就能被捕捉到,不需要每层到写try...except
因此在查找错误的源头时,调用栈非常重要。
例如:
1 2 3 4 5 6 7 8 9 10 11 def foo (s ): return 10 / int (s) def bar (s ): return foo(s) * 2 def main (): bar('0' ) main()
执行,结果如下:
1 2 3 4 5 6 7 8 9 10 11 >>> python3 err.pyTraceback (most recent call last): File "err.py" , line 11 , in <module> main() File "err.py" , line 9 , in main bar('0' ) File "err.py" , line 6 , in bar return foo(s) * 2 File "err.py" , line 3 , in foo return 10 / int (s) ZeroDivisionError: division by zero
出错并不可怕,可怕的是不知道哪里出错了。解读错误信息是定位错误的关键。我们从上往下可以看到整个错误的调用函数链:
错误信息第1行:
1 Traceback (most recent call last):
告诉我们这是错误的跟踪信息。
第2~3行:
1 2 File "err.py" , line 11 , in <module> main()
调用main()
出错了,在代码文件err.py
的第11行代码,但原因是第9行:
1 2 File "err.py" , line 9 , in main bar('0' )
调用bar('0')
出错了,在代码文件err.py
的第9行代码,但原因是第6行:
1 2 File "err.py" , line 6 , in bar return foo(s) * 2
原因是return foo(s) * 2
这个语句出错了,但这还不是最终原因,继续往下看:
1 2 File "err.py" , line 3 , in foo return 10 / int (s)
原因是return 10 / int(s)
这个语句出错了,这是错误产生的源头,因为下面打印了:
1 ZeroDivisionError: integer division or modulo by zero
根据错误类型ZeroDivisionError
,我们判断,int(s)
本身并没有出错,但是int(s)
返回0
,在计算10 / 0
时出错,至此,找到错误源头。
出错的时候,一定要分析错误的调用栈信息,才能定位错误的位置。
错误记录
如果我们既想要记录错误,有希望代码能够继续允许,可以使用python为我们提供的logging
库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import loggingdef 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' )
程序打印完异常信息后会继续执行,并正常退出:
1 2 3 4 5 6 7 8 9 10 11 >>> python3 err_logging.pyERROR: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
通过配置,logging
还可以把错误记录到日志文件里,方便事后排查。
抛出错误
既然错误时类,这就意味着我们可以定义自己的异常,并在适当的时候抛出:
1 2 3 4 5 6 7 8 9 10 11 class FooError (ValueError ): pass def foo (s ): n = int (s) if n==0 : raise FooError('invalid value: %s' % s) return 10 / n foo('0' )
执行该文件得到结果:
1 2 3 4 5 6 7 >>> python3 err_raise.py Traceback (most recent call last): File "err_throw.py" , line 11 , in <module> foo('0' ) File "err_throw.py" , line 8 , in foo raise FooError('invalid value: %s' % s) __main__.FooError: invalid value: 0
但除非必须,否则还是推荐使用python内置的错误类型。
此外,有时我们使代码高内聚松耦合,不会在出错的地方就地处理异常,而是将其抛出,让更高层级去处理,所以有时我们会这么写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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()
raise
语句如果不带参数,就会把当前错误原样抛出。
在except
中raise
一个Error,还可以用来把一种类型的错误转化成另一种类型:
1 2 3 4 try : 10 / 0 except ZeroDivisionError: raise ValueError('input error!' )
注意不要将一个错误转换为好不相关的错误,比如IOError
转换成ValueError
。
调试
调试能力对于一个工具来说十分重要,因为大多数情况下,我们写的代码不能一次通过,需要反复调试该bug。Python也有自己的调试方法。
print
最快捷的调试方式我认为就是使用print来检查哪个值出了问题,或是在哪个地方出了问题,初学者也会经常使用。
但是使用print会带来一个问题,就是测试完毕要删掉他们,如果不删掉。结果会产生大量。
断言
于是python中提供了一种代替print的方法assert
断言
1 2 3 4 5 6 7 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
:
1 2 3 4 $ python err.pyTraceback (most recent call last): ... AssertionError: n is zero!
程序中如果到处充斥着assert
,和print()
相比也好不到哪去。不过,启动Python解释器时可以用-O
参数来关闭assert
:
1 2 3 4 $ python -O err.pyTraceback (most recent call last): ... ZeroDivisionError: division by zero
logging
把print()
替换为logging
是第3种方式,和assert
比,logging
不会抛出错误,而且可以输出到文件:
1 2 3 4 5 6 7 import loggings = '0' n = int (s) logging.info('n = %d' % n) print (10 / n)
输出如下:
1 2 3 4 Traceback (most recent call last): File "E:\Programing\Python\Hello\Hello.py" , line 7 , in <module> print(10 / n) ZeroDivisionError: division by zero
我们发先只有错误类型,想要输出的logging.info('n = %d' % n)
并没有输出。
需要做如下设置:
1 2 3 4 5 6 7 8 import logginglogging.basicConfig(level=logging.INFO) s = '0' n = int (s) logging.info('n = %d' % n) print (10 / n)
结果如下:
1 2 3 4 5 INFO:root:n = 0 Traceback (most recent call last): File "E:\Programing\Python\Hello\Hello.py" , line 8 , in <module> print(10 / n) ZeroDivisionError: division by zero
这就是logging
的好处,它允许你指定记录信息的级别,有DEBUG
,INFO
,WARNING
,ERROR
等几个级别,当我们指定level=INFO
时,logging.debug
就不起作用了。同理,指定level=WARNING
后,debug
和info
就不起作用了。这样一来,你可以放心地输出不同级别的信息,也不用删除,最后统一控制输出哪个级别的信息。
logging
的另一个好处是通过简单的配置,一条语句可以同时输出到不同的地方,比如console和文件。
pbd
第4种方式是启动Python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态。
对于如下代码:
1 2 3 4 s = '0' n = int (s) print (10 / n)
用如下方式运行代码:
1 $ python -m pdb Hello.py
得到如下结果:
1 2 3 > e:\programing\python\hello\hello.py(3 )<module>() -> s = '0' (Pdb)
输入命令l
来查看代码:
1 2 3 4 5 6 7 8 (Pdb) l 1 2 3 -> s = '0' 4 n = int(s) 5 print(10 / n) [EOF ] (Pdb)
输入命令n
可以单步执行代码:
1 2 3 4 5 6 7 (Pdb) n > e:\programing\python\hello\hello.py(4 )<module>() -> n = int(s) (Pdb) n > e:\programing\python\hello\hello.py(5 )<module>() -> print(10 / n) (Pdb)
任何时候都可以输入命令p 变量名
来查看变量:
1 2 3 4 (Pdb) p s '0' (Pdb) p n 0
输入命令q
结束调试,退出程序:
这种通过pdb在命令行调试的方法理论上是万能的,但实在是太麻烦了,如果有一千行代码,要运行到第999行得敲多少命令啊。还好,我们还有另一种调试方法。
pdb.set_trace()
这个方法也是用pdb,但是不需要单步执行,我们只需要import pdb
,然后,在可能出错的地方放一个pdb.set_trace()
,就可以设置一个断点:
1 2 3 4 5 6 7 import pdbs = '0' n = int (s) pdb.set_trace() print (10 / n)
运行代码,程序会自动在pdb.set_trace()
暂停并进入pdb调试环境,可以用命令p
查看变量,或者用命令c
继续运行:
1 2 3 4 5 6 7 8 9 10 11 12 $ python Hello.py> e:\programing\python\hello\hello.py(7 )<module>() -> print(10 / n) (Pdb) p n 0 (Pdb) p s '0' (Pdb) c Traceback (most recent call last): File "E:\Programing\Python\Hello\Hello.py" , line 7 , in <module> print(10 / n) ZeroDivisionError: division by zero
IDE
最后,还能使用许多支持调式功能的IDE,设置断点只需要点一下就行了,十分方便
比如VSCode、PyCharm等等。
单元测试
例如我们编写一个可以通过属性来访问的dict:
1 2 3 4 5 6 7 8 9 10 11 12 13 class MyDict (dict ): def __init__ (self,**kw ): super ().__init__(**kw) def __getattr__ (self, key ): try : return self [key] except KeyError: raise AttributeError(r"'Dict' object has no attribute '%s'" % key) def __setattr__ (self, key, value ): self [key] = value
对于如上类,我们需要测试如下内容:
能否正常创建并通过访问属性的方式访问
能否通过普通dict
的方式赋值,并通过属性的方式访问
能否通过设置属性的方式进行赋值
以普通dict
方式访问不存在的key值能否正常报KeyError
异常
以属性方式访问时,能否正常报AttributeError
异常
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 import unittestfrom mydict import MyDictclass TestDict (unittest.TestCase): def test_init (self ): d = MyDict(a = 1 , b = 'test' ) self .assertEqual(d.a, 1 ) self .assertEqual(d.b, 'test' ) self .assertTrue(insinstance(d, dict )) def test_key (self ): d = MyDict() d['key' ] = 'value' self .assertEqual(d.key, 'Value' ) def test_attr (self ): d = MyDict() d.key = 'value' self .assertTrue(key in d) self .assertEqual(d['key' ], 'Value' ) def test_keyerror (self ): d = MyDict() with self .assertRaises(KeyError): value = d['empty' ] def test_attrerror (slef ): d = MyDict() with self .assertRaises(AttributeError): value = d.empty
编写单元测试时,需要编写一个单元测试类,从unittest.TestCase
继承。
以test
开头的方法就是测试方法,不以test
开头的方法不被认为是测试方法,测试的时候不会被执行。
其中类似assertEqual
的属性则是unittest.TestCase
为我们提供的断言,用来判断输出是否符合我们的期望。assertEqual
用来判断返回结果是否与预期结果相符。
1 self .assertEqual(abs (-1 ), 1 )
assertRaises
用来判断指定语句抛出的异常是否是预期类型的异常。
1 2 with self .assertRaises(AttributeError): value = d.empty
with的使用详见python基础 - Ender (enderxiao.top)
之后我们需要做的是就是运行测试。最简单的运行方式是在mydict_test.py
的最后加上两行代码:
1 2 if __name__ == '__main__' : unittest.main()
这样就可以把mydict_test.py
当做正常的python脚本运行:
另一种方法是在命令行通过参数-m unittest
直接运行单元测试:
1 2 3 4 5 6 $ python -m unittest mydict_test..... ---------------------------------------------------------------------- Ran 5 tests in 0.000 s OK
这是推荐的做法,因为这样可以一次批量运行很多单元测试,并且,有很多工具可以自动来运行这些单元测试。
另外,可以在单元测试中编写两个特殊的setUp()
和tearDown()
方法。这两个方法会分别在每调用一个测试方法的前后分别被执行。
setUp()
和tearDown()
方法有什么用呢?设想你的测试需要启动一个数据库,这时,就可以在setUp()
方法中连接数据库,在tearDown()
方法中关闭数据库,这样,不必在每个测试方法中重复相同的代码:
1 2 3 4 5 6 7 class TestDict (unittest.TestCase): def setUp (self ): print ('setUp...' ) def tearDown (self ): print ('tearDown...' )
可以再次运行测试看看每个测试方法调用前后是否会打印出setUp...
和tearDown...
。
文档测试
如果你经常阅读Python的官方文档,可以看到很多文档都有示例代码。比如re模块 就带了很多示例代码:
1 2 3 4 >>> import re >>> m = re.search('(?<=abc)def', 'abcdef') >>> m.group(0) 'def'
可以把这些示例代码在Python的交互式环境下输入并执行,结果与文档中的示例代码显示的一致。
这些代码与其他说明可以写在注释中,然后,由一些工具来自动生成文档。既然这些代码本身就可以粘贴出来直接运行,并且通过某些方式可以自动执行写在注释中的这些代码。
Python内置的“文档测试”(doctest)模块可以直接提取注释中的代码并执行测试。
doctest严格按照Python交互式命令行的输入和输出来判断测试结果是否正确。只有测试异常的时候,可以用...
表示中间一大段烦人的输出。
例如对上一章中的MyDict
进行测试可以这样进行:
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 class MyDict (dict ): ''' Simple dict but also support access as x.y style. >>> d1 = MyDict() >>> d1['x'] = 100 >>> d1.x 100 >>> d1.y = 200 >>> d1['y'] 200 >>> d2 = MyDict(a=1, b=2, c='3') >>> d2.c '3' >>> d2['empty'] Traceback (most recent call last): ... KeyError: 'empty' >>> d2.empty Traceback (most recent call last): ... AttributeError: 'MyDict' object has no attribute 'empty' ''' def __init__ (self, **kw ): super (Dict , self ).__init__(**kw) def __getattr__ (self, key ): try : return self [key] except KeyError: raise AttributeError(r"'MyDict' object has no attribute '%s'" % key) def __setattr__ (self, key, value ): self [key] = value if __name__=='__main__' : import doctest doctest.testmod()
运行:
什么输出也没有。这说明我们编写的doctest运行都是正确的。如果程序有问题,比如把__getattr__()
方法注释掉,再运行就会报错:
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 $ python Hello.py********************************************************************** File "E:\Programing\Python\Hello\Hello.py" , line 8 , in __main__.MyDict Failed example: d1.x Exception raised: Traceback (most recent call last): File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.9_3.9.1520.0_x64__qbz5n2kfra8p0\lib\doctest.py" , line 1336 , in __run exec(compile(example.source, filename, "single" , File "<doctest __main__.MyDict[2]>" , line 1 , in <module> d1.x AttributeError: 'MyDict' object has no attribute 'x' ********************************************************************** File "E:\Programing\Python\Hello\Hello.py" , line 14 , in __main__.MyDict Failed example: d2.c Exception raised: Traceback (most recent call last): File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.9_3.9.1520.0_x64__qbz5n2kfra8p0\lib\doctest.py" , line 1336 , in __run exec(compile(example.source, filename, "single" , File "<doctest __main__.MyDict[6]>" , line 1 , in <module> d2.c AttributeError: 'MyDict' object has no attribute 'c' ********************************************************************** 1 items had failures: 2 of 9 in __main__.MyDict ***Test Failed*** 2 failures.
注意到最后3行代码。当模块正常导入时,doctest不会被执行。只有在命令行直接运行时,才执行doctest。所以,不必担心doctest会在非测试环境下执行。