一、错误处理

1.1 try

Python内置了一套try...except...finally...的错误处理机制。

当我们认为某些代码可能会出错时,就可以用try来运行这段代码,如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except语句块,执行完except后,如果有finally语句块,则执行finally语句块,至此,执行完毕。

1
2
3
4
5
6
7
8
9
try:
print('try...')
r = 10 / 0
print('result:', r)
except ZeroDivisionError as e:
print('except:', e)
finally:
print('finally...')
print('END')

上面的代码在计算10 / 0时会产生一个除法运算错误:

1
2
3
4
try...
except: division by zero
finally...
END

错误应该有很多种类,如果发生了不同类型的错误,应该由不同的except语句块处理。

此外,如果没有错误发生,可以在except语句块后面加一个else,当没有错误发生时,会自动执行else语句

常见的错误类型和继承关系参考:https://docs.python.org/3/library/exceptions.html#exception-hierarchy

1.2 记录错误

Python内置的logging模块可以非常容易地记录错误信息打印出来,然后分析错误原因,同时,让程序继续执行下去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
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

1.3 抛出错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()

bar()函数中,捕获了错误打印一个ValueError!后,又把错误通过raise语句抛出去了。这种处理方式较为常见。捕获错误目的只是记录一下,便于后续追踪。但是,由于当前函数不知道应该怎么处理该错误,所以,最恰当的方式是继续往上抛,让顶层调用者去处理。

raise语句如果不带参数,就会把当前错误原样抛出。

二、调试

众所周知,程序能一次写完并正常运行的概率几乎不存在。总会有各种各样的bug,因此,需要一整套调试程序的手段来修复bug。

2.1 print

最简单粗暴的方法就是用print()把可能有问题的变量打印出来看看,用print()最大的坏处是将来还得删掉它。

2.2 断言assert

凡是用print()来辅助查看的地方,都可以用断言(assert)来替代:assert 表达式, '打印的信息',如果断言assert表达式为True,则无效果继续执行,如果断言表达式出错,则打印信息。

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

1
$ python -O err.py

2.3 logging

assert比,logging不会抛出错误,而且可以输出到文件:

1
2
3
4
5
6
7
import logging
logging.basicConfig(level=logging.INFO)

s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10 / n)

它允许你指定记录信息的级别,有debuginfowarningerror等几个级别,当我们指定level=INFO时,logging.debug就不起作用了。同理,指定level=WARNING后,debuginfo就不起作用了。这样一来,你可以放心地输出不同级别的信息,也不用删除,最后统一控制输出哪个级别的信息。

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

2.4 单步调试与断点

(1)pdb

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

1
$ python -m pdb err.py

以参数-m pdb启动后,pdb定位到下一步要执行的代码。

  • 输入命令l来查看代码
  • 输入命令n可以单步执行代码。
  • 输入命令p 变量名来查看变量
  • 输入命令q结束调试,退出程序

(2)pdb.set_trace()

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

运行代码,程序会自动在pdb.set_trace()暂停并进入pdb调试环境,可以用命令p查看变量,或者用命令c继续运行。

(3)IDE

使用一个支持调试功能的IDE,可以更好的设置断点、单步执行。

常规使用推荐Visual Studio Code:https://code.visualstudio.com/,需要安装Python插件。

大型项目推荐PyCharm:http://www.jetbrains.com/pycharm/

三、测试

3.1 单元测试

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

比如对函数abs(),我们可以编写出以下几个测试用例:

  1. 输入正数,比如11.20.99,期待返回值与输入相同;
  2. 输入负数,比如-1-1.2-0.99,期待返回值与输入相反;
  3. 输入0,期待返回0
  4. 输入非数值类型,比如None[]{},期待抛出TypeError

把上面的测试用例放到一个测试模块里,就是一个完整的单元测试。

(1)编写单元测试

编写单元测试时,我们需要编写一个测试类,从unittest.TestCase继承。

test开头的方法就是测试方法,不以test开头的方法不被认为是测试方法,测试的时候不会被执行。

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

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

对每一类测试都需要编写一个test_xxx()方法。由于unittest.TestCase提供了很多内置的条件判断,我们只需要调用这些方法就可以断言输出是否是我们所期望的。最常用的断言就是assertEqual()

1
self.assertEqual(abs(-1), 1) # 断言函数返回的结果与1相等

(2)运行单元测试

最简单的运行方式是:

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

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

1
$ python -m unittest mydict_test

3.2 文档测试

Python内置的“文档测试”(doctest)模块可以直接提取注释中的代码并执行测试。

当我们编写注释时,如果写上这样的注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)

无疑更明确地告诉函数的调用者该函数的期望输入和输出。

使用doctest进行测试:

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

运行测试程序:

1
$ python test.py

什么输出也没有。这说明我们编写的doctest运行都是正确的。