Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

python学习笔记,调试与测试

错误处理

许多高级语言中都内置了一套try...exceot...finally...的错误处理机制,比如java中的try...catch,python中也包含一套用于错误处理的代码。

try

和java中的一样,解释器会先执行try关键字中的代码,如果在某处出错,则会立即停止继续执行try中的代码段落,而转去执行对应except中的代码段,执行完后,将跳过else,如果有finally,则会执行finally中的代码。

如果没有发生错误。则解释器会执行完tryelsefinally中的所有内容

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')

由于UnicodeErrorValueError类型异常的子类异常,因此第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
# err.py:
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.py
Traceback (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
# err_logging.py

import logging

def 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.py
ERROR: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
# err_raise.py
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
# err_reraise.py

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语句如果不带参数,就会把当前错误原样抛出。

exceptraise一个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.py
Traceback (most recent call last):
...
AssertionError: n is zero!

程序中如果到处充斥着assert,和print()相比也好不到哪去。不过,启动Python解释器时可以用-O参数来关闭assert

1
2
3
4
$ python -O err.py
Traceback (most recent call last):
...
ZeroDivisionError: division by zero

logging

print()替换为logging是第3种方式,和assert比,logging不会抛出错误,而且可以输出到文件:

1
2
3
4
5
6
7
# Hello.py
import logging

s = '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
# Hello.py
import logging
logging.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的好处,它允许你指定记录信息的级别,有DEBUGINFOWARNINGERROR等几个级别,当我们指定level=INFO时,logging.debug就不起作用了。同理,指定level=WARNING后,debuginfo就不起作用了。这样一来,你可以放心地输出不同级别的信息,也不用删除,最后统一控制输出哪个级别的信息。

logging的另一个好处是通过简单的配置,一条语句可以同时输出到不同的地方,比如console和文件。

pbd

第4种方式是启动Python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态。

对于如下代码:

1
2
3
4
# Hellp.py
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 # -*- coding: utf-8 -*-
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结束调试,退出程序:

1
(Pdb) q

这种通过pdb在命令行调试的方法理论上是万能的,但实在是太麻烦了,如果有一千行代码,要运行到第999行得敲多少命令啊。还好,我们还有另一种调试方法。

pdb.set_trace()

这个方法也是用pdb,但是不需要单步执行,我们只需要import pdb,然后,在可能出错的地方放一个pdb.set_trace(),就可以设置一个断点:

1
2
3
4
5
6
7
# Hello.py
import pdb

s = '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
# mydict.py
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

对于如上类,我们需要测试如下内容:

  1. 能否正常创建并通过访问属性的方式访问
  2. 能否通过普通dict的方式赋值,并通过属性的方式访问
  3. 能否通过设置属性的方式进行赋值
  4. 以普通dict方式访问不存在的key值能否正常报KeyError异常
  5. 以属性方式访问时,能否正常报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 unittest

from mydict import MyDict

class 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) # 断言函数返回的结果与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脚本运行:

1
$ python mydict_test.py

另一种方法是在命令行通过参数-m unittest直接运行单元测试:

1
2
3
4
5
6
$ python -m unittest mydict_test
.....
----------------------------------------------------------------------
Ran 5 tests in 0.000s

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
# Hello.py
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()

运行:

1
$ python Hello.py

什么输出也没有。这说明我们编写的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会在非测试环境下执行。

评论