文章目录
本文章参考廖雪峰官网:错误、调试和测试 - 廖雪峰的官方网站 (liaoxuefeng.com)
一、概述
-
在程序运行中,总会遇到各种各样的错误,有的错误是在程序编写时有问题造成的,例如:
本来应该输出整数却输出了字符串
这种错误通常称之为
BUG
,BUG是必须要修复的 -
除了在编写时导致的错误,还有的错误是在用户输入是造成的,例如:
程序让用户输入一个邮箱地址,而用户输入了一串空字符串
这样的错误我们可以通过检查用户输入的数据来避免
-
还有一种错误,是完全无法在程序运行过程中预测的,例如:
写入磁盘的时候,磁盘空间满了,从网络抓取数据时,网络断开了
这种错误也叫做
异常
,在程序中通常是必须要做处理的,否则,程序会因为各种问题中止退出 -
而在Python中,Python内置了一套
异常处理机制
,帮助我们进行错误处理,除此之外,我们也需要跟踪程序的执行,查看变量的值是否正确,而这个过程就叫做调试
,Python中的pdb可以让我们以单步方式执行代码 -
最后,编写测试也很重要,有了良好的测试,就可以在程序修改后反复运行,确保程序输出符合我们的要求
二、错误处理
-
在程序运行过程中,如果发生了错误,可以实现返回一个错误代码,这样就可以知道是否存在错误以及出现错误的原因
-
在操作系统提供的调用中,返回错误代码是非常常见的,例如函数
open()
,这个函数可以打开一个文件,成功会返回一个文件描述符即一个整数
,失败则会返回-1
-
但是使用上述的错误码来表示代码是否出现错误是非常不方便的,这是因为
函数本身返回的正常结果会和错误码混在一起
,这样的结果就是我们需要写大量的代码区判断是否出现错误,例如:- 创建一个'test_1'函数,模仿传入参数不正确时,返回不一样的数据,然后使用'test_2'函数进行判断 # -*- coding: utf-8 -*- def test_1(x): if x > 1: return 1 else: return (-1) def test_2(): a = test_1(2) if a == 1: return 'ok' else: return 'error' print(test_2()) #输出: ok - 更换一下参数 # -*- coding: utf-8 -*- def test_1(x): if x > 1: return 1 else: return (-1) def test_2(): a = test_1(0) if a == 1: return 'ok' else: return 'error' print(test_2()) #输出: error
-
上面只是模拟了一下,而高级语言通常都内置了一套
try...except...finally...
的错误处理机制
,Python当然也有
- try
-
下面来看一下关于
try
的案例,从而了解try
的机制:#!/usr/bin/env python3 # -*- coding: utf-8 -*- try: print('try...') r = 10 / 0 #10/0会产生一个除法运算错误 print('result:', r) except ZeroDivisionError as e: #这里的 ZeroDivisionError是报错类型 print('except:', e) finally: print('finally...') print('END') - 当我们认为某些代码可能会出错时,可以使用'try'来运行这段代码,如果执行的确出错了,那么后续的代码将不会执行,而是直接跳转到错误处理代码,即'except'语句块,在执行完'except'之后,如果还有'finally'语句块的话,那么就执行'finally'语句块的代码,至此执行完成 - 需要注意的是,如果'try'语句块没有发生错误,那么下一步会跳转至'finally'语句块 - 所以当try发生错误时的执行顺序是'try——>except——>finally' - 当try没有发生错误的执行顺序是'try——>finally' - 10 / 0 注释: ———————————————— >>> 10/0 Traceback (most recent call last): File "<stdin>", line 1, in <module> ZeroDivisionError: division by zero #可以发现这个报错信息和输出的报错信息是相同的 ———————————————— #输出: try... except: division by zero finally... END - 从输出可以看到,在错误发生时,'r = 10 / 0 '的后面'print('result:', r)'并没有执行,而是去了'except'语句块执行了'print('except:', e)',然后到'finally'语句块执行了'print('finally...')',最后执行了'print('END')',因为'print('END')'并不包含在'try'语句块中
-
然后我们修改一下代码,使
r
变量可以成功赋值:#!/usr/bin/env python3 # -*- coding: utf-8 -*- try: print('try...') r = 10 / 2 print('result:', r) except ZeroDivisionError as e: print('except:', e) finally: print('finally...') print('END') #输出: try... result: 5.0 finally... END - 可以看到当'try'语句块没有发生错误时,执行'try'语句块后,会跳转到'finally'语句块 - 注意,'finally'语句块可以不加
-
看过上面的案例后,有一步
except ZeroDivisionError as e:
,我们发现了10 / 0
的错误类型和except ZeroDivisionError as e:
这里的错误类型是一样的,其实错误类型还有很多种,这里只是捕获了一种错误类型 -
而想要处理不同的错误类型,就需要创建不同的
except
语句块来捕获,例如:- 修改代码,可以添加多个错误捕获,例如'ValueError' #!/usr/bin/env python3 # -*- coding: utf-8 -*- try: print('try>>>>') r = 10 / int('aaa') print('result: %s' % r) except ValueError as a: print('ValueError: %s' % a) except ZeroDivisionError as b: print('ZeroDivisionError: %s' % b) finally: print('finally>>>>') print('END>>>>') #输出: try>>>> ValueError: invalid literal for int() with base 10: 'aaa' finally>>>> END>>>> - 可以发现写了两个'except'语句块,最后只输出了'ValueError'语句块的内容,这是因为在'int('aaa')'的时候就报错了,修改一下 #!/usr/bin/env python3 # -*- coding: utf-8 -*- try: print('try>>>>') r = 10 / int('0') print('result: %s' % r) except ValueError as a: print('ValueError: %s' % a) except ZeroDivisionError as b: print('ZeroDivisionError: %s' % b) finally: print('finally>>>>') print('END>>>>') #输出: try>>>> ZeroDivisionError: division by zero finally>>>> END>>>> - 这里修改成'0',最后的输出变成了'ZeroDivisionError'语句块的代码
-
除了上面说的,如果没有
try
语句块没有发生错误时,还可以在except
语句块后面加一个else
语句块,当没有错误发生时,会执行else
语句块,例如:#!/usr/bin/env python3 # -*- coding: utf-8 -*- try: print('try>>>>') r = 10 / int('2') print('result: %s' % r) except ValueError as a: print('ValueError: %s' % a) except ZeroDivisionError as b: print('ZeroDivisionError: %s' % b) else: print('no error') print('END>>>>') #输出: try>>>> result: 5.0 no error END>>>>
-
Python的错误其实也是
类
,所有的错误类型都继承自BaseException
,所以在使用except
时需要注意,它不但会捕获该类型的错误,还会捕获其子类
,例如:#!/usr/bin/env python3 # -*- coding: utf-8 -*- try: print('try>>>>') r = 10 / int('aaa') print('result: %s' % r) except ValueError as a: print('ValueError: %s' % a) except UnicodeError as b: print('UnicodeError: %s' % b) #输出: try>>>> ValueError: invalid literal for int() with base 10: 'aaa' - 这里的'UnicodeError'语句块永远不会执行,因为'UnicodeError'是'ValueError'的子类,'UnicodeError'的错误会被'ValueError'捕获
-
Python的所有错误都是从
BaseException
类派生的,常见的错误类型和继承关系如下:官网:https://docs.python.org/3/library/exceptions.html#exception-hierarchy 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 +-- EncodingWarning +-- ResourceWarning
-
除了上面说的,使用
try...except
捕获错误还有一个好处,就是可以跨越多层调用,例如:- 使用'main()'调用'bar()','bar()'调用'foo()',这时只要'main()'捕获到错误了,就可以进行错误处理 #!/usr/bin/env python3 # -*- coding: utf-8 -*- 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...') main() #输出: Error: division by zero finally...
-
利用这个特性,我们
不需要在每个可能出错的地方捕获错误
,我们只需要在合适的层次捕获错误
即可,这样就可以减少try...except...finally
的数量
- 调用栈
-
如果错误没有被捕获,那么就会一直往上抛,最后被Python解释器捕获,例如:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- def foo(s): return 10 / int(s) def bar(a): return foo(a) * 2 def main(): bar('0') main() #输出: Traceback (most recent call last): File "c:\Users\12488\Desktop\python\pachong.py", line 12, in <module> main() File "c:\Users\12488\Desktop\python\pachong.py", line 10, in main bar('0') File "c:\Users\12488\Desktop\python\pachong.py", line 7, in bar return foo(a) * 2 File "c:\Users\12488\Desktop\python\pachong.py", line 4, in foo return 10 / int(s) ZeroDivisionError: division by zero
-
出错并不可怕,可怕的是不知道是哪里出错了,而解读错误信息是定位错误的关键步骤,错误信息从上往下看可以得到整个错误的调用函数链,下面我们来解析一些错误信息:
错误信息:
Traceback (most recent call last): File "c:\Users\12488\Desktop\python\pachong.py", line 12, in <module> main() File "c:\Users\12488\Desktop\python\pachong.py", line 10, in main bar('0') File "c:\Users\12488\Desktop\python\pachong.py", line 7, in bar return foo(a) * 2 File "c:\Users\12488\Desktop\python\pachong.py", line 4, in foo return 10 / int(s) ZeroDivisionError: division by zero
第1行:
Traceback (most recent call last): - 错误的跟踪信息
第2—3行:
File "c:\Users\12488\Desktop\python\pachong.py", line 12, in <module> main() - 调用'main()'函数出错了,在代码的12行,但原因是第10行
第4—5行:
File "c:\Users\12488\Desktop\python\pachong.py", line 10, in main bar('0') - 调用'bar('0')'时出错了,在代码的第10行,但原因是第7行
第6—7行:
File "c:\Users\12488\Desktop\python\pachong.py", line 7, in bar return foo(a) * 2 - 原因是因为'return foo(a) * 2'这个语句出错了,但这也不是错误源头
第8—10行:
File "c:\Users\12488\Desktop\python\pachong.py", line 4, in foo return 10 / int(s) ZeroDivisionError: division by zero - 原因是'return 10 / int(s)'这个语句,这是错误的源头,因为下面输出了'ZeroDivisionError: division by zero'
注意:当程序出错的时候,一定要分析错误的调用栈信息,这样可以帮助我们快速的定位错误的位置
- 记录错误
-
如果不捕获错误,自然可以让Python解释器输出错误堆栈,但是这样的话,程序也会被结束
-
我们既然能捕获错误,其实也可以在程序继续执行的情况下,把错误堆栈打印出来,然后分析错误原因
-
Python内置的
logging
模块就可以记录错误信息,下面来看案例:#!/usr/bin/env python3 # -*- coding: utf-8 -*- import logging def foo(s): return 10 / int(s) def bar(a): return foo(a) * 2 def main(): try: bar('0') except Exception as e: logging.exception(e) main() print('END>>>') #输出: ERROR:root:division by zero Traceback (most recent call last): File "c:\Users\12488\Desktop\python\pachong.py", line 13, in main bar('0') File "c:\Users\12488\Desktop\python\pachong.py", line 9, in bar return foo(a) * 2 File "c:\Users\12488\Desktop\python\pachong.py", line 6, in foo return 10 / int(s) ZeroDivisionError: division by zero END>>> - 可以看到同样是出错,但是在程序打印完报错信息后,最后的'print('END>>>')'语句还是正常执行了
-
通过配置,
logging
模块还可以把错误记录到日志文件中,方便进行分析
- 抛出错误
-
因为错误是
类
,所以其实捕获一个错误就是捕获到该类的一个实例
-
错误并不是凭空出现的,而是有意创建并抛出的
,Python的内置函数会抛出很多类型的错误,同样,我们自己编写的函数也可以抛出错误 -
如果要抛出错误,首先要根据需求,可以定义一个错误的
类
,选择好继承关系,然后使用raise
语句抛出错误,例如:#!/usr/bin/env python3 # -*- coding: utf-8 -*- class FooError(ValueError): pass def test_1(s): num = int(s) if num == 0 : raise FooError('value is %s !!!' % num) return print(10 / num) test_1('0') #输出: Traceback (most recent call last): File "f:\MY_python\Python\test2.py", line 13, in <module> test_1('0') File "f:\MY_python\Python\test2.py", line 10, in test_1 raise FooError('value is %s !!!' % num) __main__.FooError: value is 0 !!! - 可以看到会抛出我们自定义的错误,现在来修改传入的参数 #!/usr/bin/env python3 # -*- coding: utf-8 -*- class FooError(ValueError): pass def test_1(s): num = int(s) if num == 0 : raise FooError('value is %s !!!' % num) return print(10 / num) test_1('2') #输出 5.0
-
从上面的案例可以看到,代码执行后会抛出我们自定义的错误
-
一般来说只有在必要的时候才会自定义我们自己的错误类型,通常都是选择Python已有的内置错误类型
-
还有一种处理错误的方法,下面来看案例:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- def foo(s): num = int(s) if num == 0 : raise ValueError('value is %s !!' % num) return print(10 / num) def bar(): try: foo('0') except ValueError as e: print('ValueError') raise bar() #输出: ValueError Traceback (most recent call last): File "f:\MY_python\Python\test2.py", line 16, in <module> bar() File "f:\MY_python\Python\test2.py", line 11, in bar foo('0') File "f:\MY_python\Python\test2.py", line 6, in foo raise ValueError('value is %s !!' % num) ValueError: value is 0 !!
-
可以看到,在
bar()
函数中,已经捕获了错误,输了ValueError
,但是在后面又通过raise
语句把foo()
函数的错误抛出去了,这样的处理方法非常常见 -
捕获错误的目的只是记录一下,方便后续解决错误,但是,由于当前函数不知道该怎么处理该错误,所以,最恰当的方式就是
把错误往上面抛,让上一层层调用者去处理,最终让顶层调用者处理
-
raise
语句如果不带参数,就会把当前错误原因抛出 -
在
except
中,使用raise
抛出一个错误,还可以把一种类型的错误转换成另一种类型,例如:#!/usr/bin/env python3 # -*- coding: utf-8 -*- try: 10 /0 except ZeroDivisionError: raise ValueError('error!!!') #输出: Traceback (most recent call last): File "f:\MY_python\Python\test2.py", line 4, in <module> 10 /0 ZeroDivisionError: division by zero During handling of the above exception, another exception occurred: Traceback (most recent call last): File "f:\MY_python\Python\test2.py", line 6, in <module> raise ValueError('error!!!') ValueError: error!!! - 可以看到'except'使用的错误类型是'ZeroDivisionError',但是最终抛出的错误类型是'ValueError'
-
注意:
- Python内置的
try...except...finally
用来处理错误十分方便。但是在出错时,会分析错误信息并定位错误发生的代码位置才是最关键也是最重要的 - 在编写程序时,可以使程序主动抛出错误,让调用者来处理相应的错误。但是,应该在文档中写清楚可能会抛出哪些错误,以及错误产生的原因
- Python内置的
三、调试
- 在实际工作中,一次性能写完代码并且可以正常运行的概率很小,因为总会有各种各样的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() #再次执行,发现语句没有输出了