• 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')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

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

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

    错误应该有很多种类,如果发生了不同类型的错误,应该由不同的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')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

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

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

    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')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    assert的意思是,表达式n != 0应该是True,否则打印信息。

    如果断言失败,assert语句本身就会抛出AssertionError,这样程序中如果到处充斥着assert,和print()相比也好不到哪去。不过,启动Python解释器时可以用-O参数来关闭assert

    $ python -O err.py
    
    • 1

    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

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

    2.4 单步调试与断点

    (1)pdb

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

    $ python -m pdb err.py
    
    • 1

    以参数-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()
    
    • 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

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

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

    (2)运行单元测试

    最简单的运行方式是:

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

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

    $ python -m unittest mydict_test
    
    • 1

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

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

    使用doctest进行测试:

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

    运行测试程序:

    $ python test.py
    
    • 1

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

  • 相关阅读:
    【 大数据分析Hadoop + Spark 】10分钟搭建Hadoop(伪分布式 )+ Spark(Local模式)环境
    自己编写cmake config.cmake
    uniapp小程序解决不能上传文件/图片问题
    vue2和vue3的声明周期
    [附源码]计算机毕业设计springboot茶叶销售微信小程序
    yolov8中train.py、val.py、predict.py的区别,什么时候该用哪个?
    【计算机网络复习】第六章 关系数据理论 1
    Linux - 进程的优先级 和 如何使用优先级调度进程
    你的知识库能提高工作效率的7个原因
    协同过滤电影推荐系统 计算机竞赛
  • 原文地址:https://blog.csdn.net/weixin_44543463/article/details/125628270