唐抉的个人博客

python学习笔记(八)

字数统计: 2.5k阅读时长: 11 min
2022/10/12

错误、调试和测试

错误处理

在程序运行过程中,可以实现设定发生错误时返回一个错误代码,这样就可以知道是否出错及其出错原因是什么。

错误处理机制python的错误处理机制:try...except...finally..

当认为某段代码可能会出错时,用try来运行这段代码,如果执行出错,则后续的代码不会继续执行,而是直接跳转到错误处理代码except语句块中,执行完except语句块后,如果还有finally语句块,则执行语句块,错误处理机制执行完毕。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
try:
print('try...')
r=10/int('a')
print('result:',r)
except ValueError as e:
print('ValueError:',e)
except ZeroDivisionError as e:
print('except:',e)
else:
print('no error!')
finally:
print('finally...')
print('END')

#运行结果如下:
try...
ValueError: invalid literal for int() with base 10: 'a'
finally...
END

如果没有错误发生,except语句块不会被执行,但如果有finally语句块,finally语句块一定会被执行。在使用except时要注意先写子类的异常,基类的异常在后。

使用try...except捕获错误可以跨越多层调用。不需要在每个可能出错的地方去捕获错误。

调用栈

若错误没有被捕获,就会一直往上抛,直至最后被python解释器捕获,打印一个错误信息,程序就退出了。出错的时候一定要分析错误的调用栈信息,才能定位错误的位置。

记录错误

如果能捕获错误,就可以把错误堆栈打印出来,分析错误原因的同时,让程序继续执行下去。

logging模块可以记录错误信息。导包后的调用形式:logging.exception(e)

抛出错误

因为错误是class,捕获一个错误就是捕获到该class的一个实例。因此自己编写的函数也可以抛出错误。若需要抛出错误,可以先定义一个错误的class,然后用raise语句抛出一个错误。但只有在必要的时候才定义自己的错误类型,若可以选择内置的错误类型,尽量使用内置的错误类型。

抛出错误例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class FooError(ValueError):
pass
def foo(s):
n=int(s)
if n==0:
raise FooError('invalid value: %s' % s)
return 10/n
foo('0')

#运行结果如下:
Traceback (most recent call last):
File "main.py", line 8, in <module>
foo('0')
File "main.py", line 6, in foo
raise FooError('invalid value: %s' % s)
__main__.FooError: invalid value: 0

另一种错误处理方式为,捕获错误后,又把错误通过raise语句往上抛,让顶层的调用者去处理。

raise语句如果不带参数,就会把当前错误原样抛出。若在exceptraise一个Error,可以将一种类型的错误转化为另一种类型,但不能将一个IOError转换成毫不相干的ValueError

练习题

运行下面的代码,根据异常信息进行分析,定位出错误源头,并修复:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from functools import reduce

def str2num(s):
return int(s)

def calc(exp):
ss = exp.split('+')
ns = map(str2num, ss)
return reduce(lambda acc, x: acc + x, ns)

def main():
r = calc('100 + 200 + 345')
print('100 + 200 + 345 =', r)
r = calc('99 + 88 + 7.6')
print('99 + 88 + 7.6 =', r)

main()

修复后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from functools import reduce

def str2num(s):
return float(s)

def calc(exp):
ss = exp.split('+')
ns = map(str2num, ss)
return reduce(lambda acc, x: acc + x, ns)

def main():
r = calc('100 + 200 + 345')
print('100 + 200 + 345 =', r)
r = calc('99 + 88 + 7.6')
print('99 + 88 + 7.6 =', r)

main()

#运行结果如下:
100 + 200 + 345 = 645.0
99 + 88 + 7.6 = 194.6

调试

方法一:print()

print()把可能有问题的变量打印出来看

方法二:断言(assert)

凡是用print()来看的地方,都可以用断言(assert)来替代。如:

1
2
3
4
5
6
7
8
9
10
11
12
def foo(s):
n=int(s)
assert n!=0,'n is zero!'#若n=0,则会抛出AssertionError错误
return 10/n
foo('0')
#运行结果如下:
Traceback (most recent call last):
File "main.py", line 5, in <module>
foo('0')
File "main.py", line 3, in foo
assert n!=0,'n is zero!'
AssertionError: n is zero!

python解释器中可以用python -O 文件名来关闭assert。关闭后,所有的assert语句都相当于一个pass。

方法三:logging

print()替换为logging后,与assert比,logging不会抛出错误,且可以输出到文件。通过配置logging,可以将一条语句同时输出到不同的地方,如文件和控制台。

1
2
3
4
5
6
7
8
9
10
11
12
13
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 "main.py", line 7, in <module>
print(10/n)
ZeroDivisionError: division by zero

logging有debug、info、warning、error等几个级别,当指定level=INFO时,logging.debug就不起作用了。当指定level=WARNING时,logging.debuglogging.INFO就不起作用了。

方法四:pdb

启动python的调制器pdb,可以让程序以单步方式运行,类似于c++的单点调试,可以随时查看代码的运行状态。pdb需要在控制台里输入语句,其形式为python -m pdb 文件名

示例如下:

pdb内的命令用法如下:

单步执行代码:输入 n

查看变量:p 变量名

退出程序:输入 q

方法五:pdb.set_trace()

该方法也是用pdb,需要在控制台里输入语句,但不需要单步执行。类似于c++的断点调试,只需要import pdb,然后在可能出错的地方一个pdb.set_trace(),就可以设置一个断点。

示例如下:

进入pdb调试环境后,可用p 变量名查看变量,输入c继续运行。

方法六:IDE

高效率的设置断点、单步执行,需要一个支持调试功能的IDE。目前常用的Python IDE有:

Visual Studio Code、PyCharm、加上pydev插件的Eclipse。

单元测试

单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。将针对某函数写的测试用例放到一个测试模块里,便是一个完整的单元测试。

若单元测试通过,则说明所测试的函数能够正常工作;若单元测试不通过,要么函数有bug,要么测试条件输入不正确。

单元测试需要引用unittest模块。常调用模块内置的条件判断来断言输出是否是期望值,最常用的断言语句是:self.assertEqual(函数名(参数),期待值)

另一种重要的断言是期待抛出指定类型的Error,用法是:

1
2
with self.assertRaises(指定类型的Error):
value=函数.属性或者是函数(参数)

运行单元测试

方法一:在代码最后加上两行代码,直接运行程序

1
2
if __name__=='__main__':
unittest.main()

方法二:命令行通过参数python -m unittest 文件名运行单元测试。

setUptearDown

每调用一个测试方法前被执行:setUp()

在测试方法执行完后被执行:tearDown()

练习题

对Student类编写单元测试,结果发现测试不通过,请修改Student类,让测试通过:

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
# -*- coding: utf-8 -*-
import unittest
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
def get_grade(self):
if self.score >= 60:
return 'B'
if self.score >= 80:
return 'A'
return 'C'
class TestStudent(unittest.TestCase):

def test_80_to_100(self):
s1 = Student('Bart', 80)
s2 = Student('Lisa', 100)
self.assertEqual(s1.get_grade(), 'A')
self.assertEqual(s2.get_grade(), 'A')

def test_60_to_80(self):
s1 = Student('Bart', 60)
s2 = Student('Lisa', 79)
self.assertEqual(s1.get_grade(), 'B')
self.assertEqual(s2.get_grade(), 'B')

def test_0_to_60(self):
s1 = Student('Bart', 0)
s2 = Student('Lisa', 59)
self.assertEqual(s1.get_grade(), 'C')
self.assertEqual(s2.get_grade(), 'C')

def test_invalid(self):
s1 = Student('Bart', -1)
s2 = Student('Lisa', 101)
with self.assertRaises(ValueError):
s1.get_grade()
with self.assertRaises(ValueError):
s2.get_grade()

if __name__ == '__main__':
unittest.main()

修改后的代码为:

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
import unittest
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
def get_grade(self):
if self.score >= 60 and self.score < 80:
return 'B'
elif self.score >= 80 and self.score <= 100:
return 'A'
elif self.score >= 0 and self.score < 60:
return 'C'
else:
raise ValueError()

class TestStudent(unittest.TestCase):

def test_80_to_100(self):
s1 = Student('Bart', 80)
s2 = Student('Lisa', 100)
self.assertEqual(s1.get_grade(), 'A')
self.assertEqual(s2.get_grade(), 'A')

def test_60_to_80(self):
s1 = Student('Bart', 60)
s2 = Student('Lisa', 79)
self.assertEqual(s1.get_grade(), 'B')
self.assertEqual(s2.get_grade(), 'B')

def test_0_to_60(self):
s1 = Student('Bart', 0)
s2 = Student('Lisa', 59)
self.assertEqual(s1.get_grade(), 'C')
self.assertEqual(s2.get_grade(), 'C')

def test_invalid(self):
s1 = Student('Bart', -1)
s2 = Student('Lisa', 101)
with self.assertRaises(ValueError):
s1.get_grade()
with self.assertRaises(ValueError):
s2.get_grade()

if __name__ == '__main__':
unittest.main()

#运行结果如下:
....
----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

文档测试

文档测试(doctest)模块可以自动提取写在注释中的代码并执行测试。

dectest严格按照命令行的输入和输出来判断测试结果是否正确。若运行文档测试后没有输出,则说明程序是正确的,在测试异常时,可以用...表示中间的出错的信息。

运行文档测试,需要先在代码的最后加上一下三行代码,然后在命令行中通过python 文件名来执行文档测试:

1
2
3
if __name__=='__main__':
import doctest
doctest.testmod()

在模块正常导入时不会执行文档测试,只有在命令行直接运行时才执行文档测试。

练习题

对函数fact(n)编写doctest并执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def fact(n):
'''
Calculate 1*2*...*n

>>> fact(1)
1
>>> fact(10)
?
>>> fact(-1)
?
'''
if n < 1:
raise ValueError()
if n == 1:
return 1
return n * fact(n - 1)

if __name__ == '__main__':
import doctest
doctest.testmod()

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def fact(n):
'''
Calculate 1*2*...*n

>>> fact(1)
1
>>> fact(10)
3628800
>>> fact(-1)
Traceback (most recent call last):
...
ValueError
'''
if n < 1:
raise ValueError()
if n == 1:
return 1
return n * fact(n - 1)

if __name__ == '__main__':
import doctest
doctest.testmod()
#运行结果为空
CATALOG
  1. 1. 错误、调试和测试
    1. 1.1. 错误处理
      1. 1.1.1. 错误处理机制python的错误处理机制:try...except...finally..。
      2. 1.1.2. 调用栈
      3. 1.1.3. 记录错误
      4. 1.1.4. 抛出错误
        1. 1.1.4.1. 练习题
    2. 1.2. 调试
      1. 1.2.1. 方法一:print()
      2. 1.2.2. 方法二:断言(assert)
      3. 1.2.3. 方法三:logging
      4. 1.2.4. 方法四:pdb
      5. 1.2.5. 方法五:pdb.set_trace()
      6. 1.2.6. 方法六:IDE
    3. 1.3. 单元测试
      1. 1.3.1. 运行单元测试
      2. 1.3.2. setUp与tearDown
      3. 1.3.3. 练习题
    4. 1.4. 文档测试
      1. 1.4.1. 练习题