Python复习笔记3——测试与调试技巧

一、错误处理

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运行都是正确的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Huffiee

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值