三、调试
- 在实际工作中,一次性能写完代码并且可以正常运行的概率很小,因为总会有各种各样的bug需要处理
- 有的bug很简单,看看错误输出就可以解决,有的bug很复杂,需要知道出错时,哪些变量是正确的,哪些变量的值是错误的,因此,需要一整套调试程序的手段来修复bug
- 下面来看几种调试的方法:
- 第一种方法简单粗暴,就是把
print()
把可能有问题的变量打印出来看看,例如:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- def foo(s): num = int(s) print('>>>num = %s' % num) return print(10 / num) def main(): foo('0') main() #输出 >>>num = 0 #输出了num变量的值 Traceback (most recent call last): File "f:\MY_python\Python\test2.py", line 11, in <module> main() File "f:\MY_python\Python\test2.py", line 9, in main foo('0') File "f:\MY_python\Python\test2.py", line 6, in foo return print(10 / num) ZeroDivisionError: division by zero - 但是使用'print()'最大的坏处就是在调试完代码之后,需要把'print()'语句注释或者删除掉,一想到程序里各种'print()',运行结果里面包含很多打印的无关信息,这个时候我们可以使用第二种方法
- 断言assert
- 只要是使用了
print()
语句来辅助查看的地方,都可以使用断言(assert)
来代替,例如:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- def foo(s): num = int(s) assert num != 0,'num is 0 !!' return print(10 / num) def main(): foo('0') main() #输出 Traceback (most recent call last): File "f:\MY_python\Python\test2.py", line 11, in <module> main() File "f:\MY_python\Python\test2.py", line 9, in main foo('0') File "f:\MY_python\Python\test2.py", line 5, in foo assert num != 0,'num is 0 !!' AssertionError: num is 0 !! #最后输出的是AssertionError错误类型 - 以assert num != 0,'num is 0 !!'为例,来看一下assert的使用方法: assert num != 0 ,当表达式num != 0 为'False'时,就会输出后面的语句'num is 0 !!',而assert本身输出的错误类型是AssertionError,
但是如果只是单纯的把print()换成了assert(),其实也好不到哪去,但是在启动Python解释器可以使用-O参数来关闭assert,例如:
- 加-O来执行py文件 PS F:\MY_python\Python> & C:/Users/12488/AppData/Local/Microsoft/WindowsApps/python3.10.exe -O f:/MY_python/Python/test2.py Traceback (most recent call last): File "f:\MY_python\Python\test2.py", line 11, in <module> main() File "f:\MY_python\Python\test2.py", line 9, in main foo('0') File "f:\MY_python\Python\test2.py", line 6, in foo return print(10 / num) ZeroDivisionError: division by zero #可以看到错误类型不再是AssertionError
- 关闭
assert
后,可以把所有的assert
语句当作pass
看待
- logging
- 把
print()
替换为logging
是第三种方式,和assert
相比,logging
不会抛出错误,但是可以输出到文件,例如:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- import logging s = '0' n = int(s) logging.info('n = %d' % n) print(10/n) #输出 Traceback (most recent call last): File "f:\MY_python\Python\test2.py", line 8, in <module> print(10/n) ZeroDivisionError: division by zero - 使用'logging.info()'就可以输出一段文本,但是从上面的代码可以看出来,使用了'logging.info()'之后并没有输出相应的文本,这是因为logging有等级之分,这个时候需要再加一段代码 #!/usr/bin/env python3 # -*- coding: utf-8 -*- 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 "f:\MY_python\Python\test2.py", line 9, in <module> print(10/n) ZeroDivisionError: division by zero - 成功输出了相应文本
使用logging时,允许指定记录信息的级别,总共有debug、info、warning、error、critical等五个级别,其中默认级别是warning,上面使用了INFO级别
日志等级说明:
'DEBUG':程序调试bug时使用 'INFO':程序正常运行时使用 'WARNING':程序未按预期运行时使用,但并不是错误,例如:用户登录时密码错误 'ERROR':程序出错误时使用,例如:IO操作失败 'CRITICAL':特别严重的问题,导致程序不能再继续运行时使用,例如:磁盘空间为空,一般很少使用
注意:
在logging中,根据等级从低到高的顺序是:
DEBUG < INFO < WARNING < ERROR < CRITICAL
在定义
logging
的等级时,输出等级如果大于等于定义的等级,最终还是会继续输出,例如:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- import logging logging.basicConfig(level=logging.INFO) s = '0' n = int(s) logging.info('n = %d' % n) logging.debug('n = %d' % n) logging.warning('n = %d' % n) logging.error('n = %d' % n) logging.critical('n = %d' % n) print(10/n) #输出 INFO:root:n = 0 WARNING:root:n = 0 ERROR:root:n = 0 CRITICAL:root:n = 0 Traceback (most recent call last): File "f:\MY_python\Python\test2.py", line 13, in <module> print(10/n) ZeroDivisionError: division by zero - 可以看到定义的等级是'INFO',其中'DEBUG'等级比'INFO'低,所以'logging.debug('n = %d' % n)'语句没有输出,修改一下代码 #!/usr/bin/env python3 # -*- coding: utf-8 -*- import logging logging.basicConfig(level=logging.DEBUG) s = '0' n = int(s) logging.info('n = %d' % n) logging.debug('n = %d' % n) logging.warning('n = %d' % n) logging.error('n = %d' % n) logging.critical('n = %d' % n) print(10/n) #输出 INFO:root:n = 0 DEBUG:root:n = 0 WARNING:root:n = 0 ERROR:root:n = 0 CRITICAL:root:n = 0 Traceback (most recent call last): File "f:\MY_python\Python\test2.py", line 13, in <module> print(10/n) ZeroDivisionError: division by zero - 可以看到,因为'DEBUG'等级就是最低的了,所以其他等级的输出都可以正常输出,再次修改代码 #!/usr/bin/env python3 # -*- coding: utf-8 -*- import logging logging.basicConfig(level=logging.CRITICAL) s = '0' n = int(s) logging.info('n = %d' % n) logging.debug('n = %d' % n) logging.warning('n = %d' % n) logging.error('n = %d' % n) logging.critical('n = %d' % n) print(10/n) #输出: CRITICAL:root:n = 0 Traceback (most recent call last): File "f:\MY_python\Python\test2.py", line 13, in <module> print(10/n) ZeroDivisionError: division by zero - 这里因为'CRITICAL'等级就是最高的了,所以其他等级的输出信息都无法输出了,现在来看如果不指定等级 #!/usr/bin/env python3 # -*- coding: utf-8 -*- import logging #logging.basicConfig(level=logging.CRITICAL) s = '0' n = int(s) logging.info('n = %d' % n) logging.debug('n = %d' % n) logging.warning('n = %d' % n) logging.error('n = %d' % n) logging.critical('n = %d' % n) print(10/n) #输出 WARNING:root:n = 0 ERROR:root:n = 0 CRITICAL:root:n = 0 Traceback (most recent call last): File "f:\MY_python\Python\test2.py", line 13, in <module> print(10/n) ZeroDivisionError: division by zero - 可以看到,当不指定输出,因为默认等级就是'WARNING'所以'info'和'debug'就无法正常输出了
- pdb
- 第四种方式就是启动python的
调试器pdb
,让程序以单步方式
运行,可以随时查看运行状态 如果有人用过ansible的话,ansible-playbook --step也可以单步执行剧本
- 下面来看案例:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- s = '0' n = int(s) print(10 / n ) #执行: python -m pdb .py文件名称 PS F:\MY_python\Python> & C:/Users/12488/AppData/Local/Microsoft/WindowsApps/python3.10.exe -m pdb f:/MY_python/Python/test2.py > f:\my_python\python\test2.py(3)<module>() #进入调试器 -> s = '0' #第一行代码 (Pdb) l #使用 ' l '可以查看当前所有代码 1 #!/usr/bin/env python3 2 # -*- coding: utf-8 -*- 3 -> s = '0' 4 n = int(s) 5 print(10 / n ) [EOF] (Pdb) n #使用' n '继续下一步 > f:\my_python\python\test2.py(4)<module>() -> n = int(s) (Pdb) n > f:\my_python\python\test2.py(5)<module>() -> print(10 / n ) (Pdb) n ZeroDivisionError: division by zero > f:\my_python\python\test2.py(5)<module>() -> print(10 / n ) (Pdb) p n #使用' p 变量名 '可以查看当前变量的值 0 (Pdb) p s '0' (Pdb) n ZeroDivisionError: division by zero > <string>(1)<module>()->None (Pdb) q #使用' q '退出程序 PS F:\MY_python\Python>
- 使用pdb方法再命令行调试理论上是万能的,但是如果代码有好几千行,使用pdb方法显然就不太可以了
- pdb.set_trace()
这个方法也是使用pdb,但是无需单步执行,我们只需要在代码导入模块import pdb
即可,然后再可能会出错的地方放一个pdb.set_trace()
,就可以设置一个断点,例如:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- import pdb s = '0' n = int(s) pdb.set_trace() #再运行到这里时会暂停,并且进入pdb调试环境 print(10 / n ) #执行 PS F:\MY_python\Python> & C:/Users/12488/AppData/Local/Microsoft/WindowsApps/python3.10.exe f:/MY_python/Python/test2.py > f:\my_python\python\test2.py(8)<module>() -> print(10 / n ) (Pdb) p n #可以使用' p 变量名 ' 查看值 0 (Pdb) c #继续运行剩余代码 Traceback (most recent call last): File "f:\MY_python\Python\test2.py", line 8, in <module> print(10 / n ) ZeroDivisionError: division by zero
- 这样的方式虽然比直接使用pdb效率好一点,但是也是二斤八两
- IDE
- 除了上面的调试方法,我们还可以直接下载一个支持调试功能的
IDE
,目前较好的有:VS code
和PyCharm
,我自己使用的就是VS code
四、单元测试
如果你听过测试驱动开发(TDD:Test-Driven Development),那么单元测试就不会陌生
注释:测试驱动开发,英文全称Test-Driven Development,简称TDD,是一种不同于传统 软件开发流程 的新型的开发方法。 它要求在 编写 某个 功能 的 代码 之前先编写测试代码,然后只编写使测试通过的功能代码,通过测试来推动整个开发的进行。
单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验测试工作的,例如:
- 以'abs()'函数为例,我们来写几个测试用例: 1、输入正数,例如1、1.2、0.99,期待返回值与输入相同 2、输入负数,例如-1、-1.2、-0.99,期待返回值与输入相反 3、输入0,期待返回0 4、输入非数值类型,比如None、列表、字典等,期待抛出错误TypeError
把上述的测试用例放到一个测试模块里,就是一个完整的单元测试
如果单元测试通过,说明测试的函数能够正常工作,如果单元测试不通过,那么就需要看函数是不是又bug,是不是测试条件输入不正确,最终需要使测试单元能够成功通过
单元测试通过后,如果我们对abs()函数做了修改,只需要再跑一遍单元测试,如果通过,说明我们进行的修改没有对函数原有的行为造成影响,反之,测试不通过,就说明我们进行的修改对函数原有的行为造成了影响,这个时候就需要根据需求修改函数或者修改测试
这种以测试为驱动的开发模式最大的好处就是确保一个程序模块的行为符合我们设计的测试用例,在将来修改时,可以极大程度的保证该模块的行为仍然是正确的
这里不细说了,感兴趣的可以去看一下原文,原文讲述了如何进行单元测试,以及单元测试框架unittest的简单使用
五、文档测试
- 如果经常阅读Python的官方文档,会看到很多文档都会有示例代码,例如:
>>> import re >>> m = re.search('(?<=abc)def', 'abcdef') >>> m.group(0) 'def'
在自己动手在交互环境执行时,会发现输出结果是一样的,这些代码与其他说明可以写在注释中,然后又一些工具自动生成文档
既然这些代码本身就可以粘贴出来直接运行,那么是不是也可以自动执行写在注释中的这些代码呢,肯定是可以的,例如:
- 当我们编写注释时,如果写上这样的注释 def abs(n): ''' 获取数字绝对值的函数。 示例: >>> abs(1) 1 >>> abs(-1) 1 >>> abs(0) 0 ''' return n if n >= 0 else (-n)
像上面那样编写注释,无疑是更加明确的告诉函数调用者该函数的期望输入与输出
在Python中,Python内置的文档测试doctest模块可以直接提取注释中的代码并且执行测试
doctest严格按照Python交互式命令行的输入和输出,从而来判断测试结果是否正确,只有测试异常时,可以使用...表示中间一大段无用的输出,现在使用doctest来测试Dict类
#!/usr/bin/env python3 # -*- coding: utf-8 -*- class Dict(dict): ''' Simple dict but also support access as x.y style. >>> d1 = Dict() >>> d1['x'] = 100 >>> d1.x 100 >>> d1.y = 200 >>> d1['y'] 200 >>> d2 = Dict(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: 'Dict' 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"'Dict' object has no attribute '%s'" % key) def __setattr__(self, key, value): self[key] = value if __name__=='__main__': import doctest #导入模块 doctest.testmod() - 没有任何输出,这说明我们编写的doctest运行都是正确的,如果程序有问题,例如修改一下'__getattr__()'方法,再次运行 #!/usr/bin/env python3 # -*- coding: utf-8 -*- class Dict(dict): ''' Simple dict but also support access as x.y style. >>> d1 = Dict() >>> d1['x'] = 100 >>> d1.x 100 >>> d1.y = 200 >>> d1['y'] 200 >>> d2 = Dict(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: 'Dict' object has no attribute 'empty' ''' def __init__(self, **kw): super(Dict, self).__init__(**kw) def __getattr__(self, key): try: pass #return self[key] #注释返回值 except KeyError: raise AttributeError(r"'Dict' object has no attribute '%s'" % key) def __setattr__(self, key, value): self[key] = value if __name__=='__main__': import doctest doctest.testmod() #执行输出: ********************************************************************** File "f:\MY_python\Python\test2.py", line 9, in __main__.Dict Failed example: d1.x Expected: 100 Got nothing ********************************************************************** File "f:\MY_python\Python\test2.py", line 15, in __main__.Dict Failed example: d2.c Expected: '3' Got nothing ********************************************************************** File "f:\MY_python\Python\test2.py", line 21, in __main__.Dict Failed example: d2.empty Expected: Traceback (most recent call last): ... AttributeError: 'Dict' object has no attribute 'empty' Got nothing ********************************************************************** 1 items had failures: 3 of 9 in __main__.Dict ***Test Failed*** 3 failures.
注意
:
注意上面的最后三行代码,当模块正常导入时,doctest不会被执行,只有在命令行直接运行时,才会执行doctest,所以不用担心dotest会在非测试环境下执行
- 下面来看一个编写doctest的案例:
# -*- coding: utf-8 -*- def fact(n): ''' Calculate 1*2*...*n >>> fact(1) 1 >>> fact(10) 3628800 >>> fact(-1) Traceback (most recent call last): File "/usr/local/lib/python3.9/doctest.py", line 1336, in __run exec(compile(example.source, filename, "single", File "<doctest __main__.fact[2]>", line 1, in <module> fact(-1) File "/app/main.py", line 16, in fact raise ValueError('valueError!') ValueError: valueError! ''' if n < 1: raise ValueError('valueError!') if n == 1: return 1 return n * fact(n - 1) if __name__ == '__main__': import doctest doctest.testmod() - 执行后没有任何输出,其实只需要看报错信息修改注释即可,例如: 报错信息是这样的: ********************************************************************** File "/app/main.py", line 10, in __main__.fact Failed example: fact(-1) #这是执行的语句 Exception raised: #这个语句块下面的就是输出的结果,这个结果跟注释中写的不一样就会报错,只需要根据这个报错修改代码,或者修改注释即可 Traceback (most recent call last): File "/usr/local/lib/python3.9/doctest.py", line 1336, in __run exec(compile(example.source, filename, "single", File "<doctest __main__.fact[2]>", line 1, in <module> fact(-1) File "/app/main.py", line 17, in fact raise ValueError('valueError!') ValueError: valueError! ********************************************************************** 1 items had failures: 1 of 3 in __main__.fact ***Test Failed*** 1 failures. 修改代码后: # -*- coding: utf-8 -*- def fact(n): ''' Calculate 1*2*...*n >>> fact(1) 1 >>> fact(10) 3628800 >>> fact(-1) Traceback (most recent call last): File "/usr/local/lib/python3.9/doctest.py", line 1336, in __run exec(compile(example.source, filename, "single", File "<doctest __main__.fact[2]>", line 1, in <module> fact(-1) File "/app/main.py", line 17, in fact raise ValueError('valueError!') ValueError: valueError! ''' if n < 1: raise ValueError('valueError!') if n == 1: return 1 return n * fact(n - 1) if __name__ == '__main__': import doctest doctest.testmod() #再次执行,发现语句没有输出了