0%

Python - Theme 7 Error and Debugging

1. 错误处理

1.1. 异常

异常: 因为程序出现了错误而在正常控制流以外采取的行为
异常也是一个类,类的继承关系:https://docs.python.org/3/library/exceptions.html#exception-hierarchy

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
def div(a, b):
try:
print(a / b) #在try块中出现错误会直接去找except块,不执行try块中剩下代码
print(b)
except ZeroDivisionError: #精准捕获后可自定义输出提示
print("Error: b should not be 0 !!")
except Exception as e: #没有被捕获的异常全部在Exception中,这时最好将捕获到的信息输出提示
print("Unexpected Error: {}".format(e))
else: #如果没有异常会执行else语句
print('Run into else only when everything goes well')
finally: #finally语句一定会被执行; 一般会用于最后释放资源(文件、网络连接)
print('Always run into finally block.')
print("END")


#Test1
div(2, 0)
#输出
Error: b should not be 0 !!
Always run into finally block.
END

#Test2
div(2, 'bad type')
#输出
Unexpected Error: unsupported operand type(s) for /: 'int' and 'str'
Always run into finally block.
END

#Test3
div(1, 2)
#输出
0.5
2
Run into else only when everything goes well
Always run into finally block.
END

1.2. 程序的调用栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def div(a, b):
return a/b

def double(a, b):
return div(a, b) * 2

def main(a, b):
return double(a, b)

main(2, 0)

#如果没有捕获异常,则由python解释器抛出的错误。错误一直向上抛,直至被解释器捕获
Traceback (most recent call last):
File "/Users/peterson/PycharmProjects/LiaoxuefengLearn/test3.py", line 18, in <module>
print(main(2, 0))
File "/Users/peterson/PycharmProjects/LiaoxuefengLearn/test3.py", line 14, in main
return double(a, b)
File "/Users/peterson/PycharmProjects/LiaoxuefengLearn/test3.py", line 10, in double
return div(a, b) * 2
File "/Users/peterson/PycharmProjects/LiaoxuefengLearn/test3.py", line 6, in div
return a/b
ZeroDivisionError: division by zero

1.3. 在合适的地方捕获错误

python捕获可以跨越多层调用,也就是说只需要在合适的层次捕获错误即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def div(a, b):
return a/b


def double(a, b):
return div(a, b) * 2


def main(a, b):
try:
return double(a, b)
except Exception as e:
print("Unexpected Error: {}".format(e))

#Test1
main(2, 0)
#输出
Unexpected Error: division by zero

#Test2
main(2, 'bad type')
#输出
Unexpected Error: unsupported operand type(s) for /: 'int' and 'str'

1.4. 错误记录(log)

异常捕获可以把错误栈记录下来,同时让程序继续执行下去

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
import logging

def div(a, b):
return a/b

def double(a, b):
return div(a, b) * 2

def main(a, b):
try:
return double(a, b)
except Exception as e:
logging.exception(e)
print("=" * 6, 'END', "=" * 6)


main(2, 0)

#输出(在没有触发程序中断的情况下并记录了log)
====== END ======
ERROR:root:division by zero
Traceback (most recent call last):
File "/Users/xxx/test3.py", line 16, in main
return double(a, b)
File "/Users/xxx/test3.py", line 11, in double
return div(a, b) * 2
File "/Users/xxx/test3.py", line 7, in div
return a/b
ZeroDivisionError: division by zero

Process finished with exit code 0

1.5. 自定义错误类并抛出错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 一般只有在特别必要的时候才自己定义
class MyError(BaseException):
pass

def error():
raise MyError("This is an error raised by myself")

error()

#输出
Traceback (most recent call last):
File "/Users/xxx/test3.py", line 12, in <module>
error()
File "/Users/xxx/test3.py", line 10, in error
raise MyError("This is an error raised by myself")
__main__.MyError: This is an error raised by myself

2. 调试

2.1. print() - 简单粗暴

可以直接把可能有问题的变量打印出来。但是未来还需要手动删除这些print,在运行结果中也包含了过多垃圾信息。

2.2. assert - 断言

凡是用print()的地方都可以替换成assert,如果断言失败就会抛出 AssertionError。

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
def div(a, b):
assert b != 0, 'b is zero!' #设置断言
return a/b


def double(a, b):
return div(a, b) * 2


def main(a, b):
return double(a, b)
print("=" * 6, 'END', "=" * 6)


main(2, 0)

#输出
Traceback (most recent call last):
File "/Users/xxx/test3.py", line 20, in <module>
main(2, 0)
File "/Users/xxx/test3.py", line 16, in main
return double(a, b)
File "/Users/xxx/test3.py", line 12, in double
return div(a, b) * 2
File "/Users/xxx/test3.py", line 7, in div
assert b != 0, 'b is zero!'
AssertionError: b is zero! #断言在此

Process finished with exit code 1

关闭断言: python -O xx.py

1
2
3
4
5
6
7
8
9
10
11
>>> python3 -O test3.py
Traceback (most recent call last):
File "test3.py", line 20, in <module>
main(2, 0)
File "test3.py", line 16, in main
return double(a, b)
File "test3.py", line 12, in double
return div(a, b) * 2
File "test3.py", line 8, in div
return a/b
ZeroDivisionError: division by zero #这样一来就忽略了所有 assert

2.3. logging - 记录日志分析

和assert相比,logging并不会抛出错误而是记录,可以选择记录形式、保存文件等方式。

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
import logging
logging.basicConfig(level=logging.INFO)


def div(a, b):
logging.info('b=%d' % b)
return a/b


def double(a, b):
return div(a, b) * 2


def main(a, b):
return double(a, b)
print("=" * 6, 'END', "=" * 6)


main(2, 0)

#输出
INFO:root:b=0 #日志输出在此
Traceback (most recent call last):
File "/Users/xxx/test3.py", line 21, in <module>
main(2, 0)
File "/Users/xxx/test3.py", line 17, in main
return double(a, b)
File "/Users/xxx/test3.py", line 13, in double
return div(a, b) * 2
File "/Users/xxx/test3.py", line 9, in div
return a/b
ZeroDivisionError: division by zero

Process finished with exit code 1

日志配置tips:

  1. 日志级别:CRITICAL > ERROR > WARNING > INFO > DEBUG > NOTSET
  2. 可以将日志配置保存到一个文件中方便管理
  3. 自定义日志的记录格式方便日后的分析

2.4. pdb - python调试器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> python3 -m pdb test3.py #启动调试器
> /Users/peterson/PycharmProjects/LiaoxuefengLearn/test3.py(5)<module>()
-> def div(a, b):
(Pdb) n #单步调试
> /Users/peterson/PycharmProjects/LiaoxuefengLearn/test3.py(9)<module>()
-> def double(a, b):
(Pdb) n
> /Users/peterson/PycharmProjects/LiaoxuefengLearn/test3.py(13)<module>()
-> def main(a, b):
(Pdb) n
> /Users/peterson/PycharmProjects/LiaoxuefengLearn/test3.py(18)<module>()
-> main(2, 0)
(Pdb) n
ZeroDivisionError: division by zero
> /Users/peterson/PycharmProjects/LiaoxuefengLearn/test3.py(18)<module>()
-> main(2, 0)
(Pdb) q #退出

设置断点:pdb.set_trace()

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

def div(a, b):
pdb.set_trace() #程序运行到这里会自动暂停
return a/b


def double(a, b):
return div(a, b) * 2


def main(a, b):
return double(a, b)
print("=" * 6, 'END', "=" * 6)


main(2, 0)

#输出
-> return a/b
(Pdb) p b #当程序暂停到这里时,可以查看b变量的内容
0

2.5. IDE - 最佳推荐(Pycharm)

3. 单元测试

单元测试就是对一个模块、一个函数或者一个类进行正确性检测的工作。

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
import unittest

class TestStringMethods(unittest.TestCase): #编写一个测试类,继承unittest.TestCase就创建了一个测试样例

def setUp(self): #测试前置方法: 测试框架会应用于每一个测试方法
print('setUp...')

def test_upper(self): #测试方法以"test"开头
self.assertEqual('foo'.upper(), 'FOO')

def test_isupper(self):
self.assertTrue('FOO'.isupper())
self.assertFalse('Foo'.isupper())

def test_split(self):
s = 'hello world'
self.assertEqual(s.split(), ['hello', 'world'])
# check that s.split fails when the separator is not a string
with self.assertRaises(TypeError): #抛出一个特定异常
s.split(2)

def tearDown(self): #一旦setUp()运行成功,无论如何都会执行后续清理工作
print('tearDown...')


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

#输出:
>>> python test4.py -v
test_isupper (__main__.TestStringMethods) ... setUp...
tearDown...
ok
test_split (__main__.TestStringMethods) ... setUp...
tearDown...
ok
test_upper (__main__.TestStringMethods) ... setUp...
tearDown...
ok

----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

(1). 单元测试可以有效地测试某个程序的行为,也为重构代码提供了便利。
(2). 单元测试要覆盖常用的输入输出组合、边界条件和异常
(3). 单元测试要尽可能简单以避免测试代码出现bug

4. 文档测试

可以看到在很多python官方文档中都会附有示例代码,这些代码不仅可以粘贴出来在命令行执行,而且也可以直接执行。

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
"""
This is the "example" module.

The example module supplies one function, factorial(). For example,

>>> factorial(5)
120
"""

def factorial(n):
"""Return the factorial of n, an exact integer >= 0.

>>> [factorial(n) for n in range(6)]
[1, 1, 2, 6, 24, 120]
>>> factorial(30)
265252859812191058636308480000000
>>> factorial(-1)
Traceback (most recent call last):
...
ValueError: n must be >= 0

Factorials of floats are OK, but the float must be an exact integer:
>>> factorial(30.1)
Traceback (most recent call last):
...
ValueError: n must be exact integer
>>> factorial(30.0)
265252859812191058636308480000000

It must also not be ridiculously large:
>>> factorial(1e100)
Traceback (most recent call last):
...
OverflowError: n too large
"""

import math
if not n >= 0:
raise ValueError("n must be >= 0")
if math.floor(n) != n:
raise ValueError("n must be exact integer")
if n+1 == n: # catch a value like 1e300
raise OverflowError("n too large")
result = 1
factor = 2
while factor <= n:
result *= factor
factor += 1
return result


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

#输出
>>> python test4.py -v #写在注释中的示例都会被测试
Trying:
factorial(5)
Expecting:
120
ok
Trying:
[factorial(n) for n in range(6)]
Expecting:
[1, 1, 2, 6, 24, 120]
ok
Trying:
factorial(30)
Expecting:
265252859812191058636308480000000
ok
Trying:
factorial(-1)
Expecting:
Traceback (most recent call last):
...
ValueError: n must be >= 0
ok
Trying:
factorial(30.1)
Expecting:
Traceback (most recent call last):
...
ValueError: n must be exact integer
ok
Trying:
factorial(30.0)
Expecting:
265252859812191058636308480000000
ok
Trying:
factorial(1e100)
Expecting:
Traceback (most recent call last):
...
OverflowError: n too large
ok
2 items passed all tests:
1 tests in __main__
6 tests in __main__.factorial
7 tests in 2 items.
7 passed and 0 failed.
Test passed.