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会在非测试环境下执行。