目录
  1. 一、错误处理
  2. 1.1 try
  3. 1.2 记录错误
  4. 1.3 抛出错误
  5. 二、调试
  6. 2.1 print
  7. 2.2 断言assert
  8. 2.3 logging
  9. 2.4 单步调试与断点
  10. 三、测试
  11. 3.1 单元测试
  12. 3.2 文档测试

一、错误处理

1.1 try

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

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

try:
    print('try...')
    r = 10 / 0
    print('result:', r)
except ZeroDivisionError as e:
    print('except:', e)
finally:
    print('finally...')
print('END')

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

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模块可以非常容易地记录错误信息打印出来,然后分析错误原因,同时,让程序继续执行下去:

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

同样是出错,但程序打印完错误信息后会继续执行

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 抛出错误

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,则无效果继续执行,如果断言表达式出错,则打印信息。

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

$ python -O err.py

2.3 logging

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

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,让程序以单步方式运行,可以随时查看运行状态。

$ 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开头的方法不被认为是测试方法,测试的时候不会被执行。

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

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

(2)运行单元测试

最简单的运行方式是:

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

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

$ python -m unittest mydict_test

3.2 文档测试

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

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

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进行测试:

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

运行测试程序:

$ python test.py

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