• Python语法速查: 14. 测试与调优


    返回目录

    本篇索引

    (1)测试的基本概念

    (2)doctest模块

    (3)unittest模块

    (4)调试器和pdb模块

    (5)程序探查

    (6)调优与优化

      (1)测试的基本概念

    对程序的各个部分建立测试,这个称为:单元测试(unit test),更进一步的是测试驱动编程(test-driven programming)。 简单来说,就是“先写测试、再写程序”。是否一定要为所有的函数和模块编写测试代码, 这个目前仍有争论,因为构建完备的测试集本身就是一件工作量很大的事情。而且常常会发生这样的事情: 测试还没编好,需求已经变更了。但不管如何,测试驱动的理念是一种进步,掌握自动化测试工具, 比以前没有自动化测试工具的时代,效率要提高很多倍。

    ● 测试时驱动开发的4步:

    (1)指出需要的新特性,可以记录下来,然后为其编写一个测试。

    (2)编写特性的概要代码,程序代码没有任何语法错误,但测试结果会失败。 看到测试失败是很重要的,这样就能确定测试可以失败。 这里再强调一遍:在试图让测试成功之前,先要看到它失败!

    (3)为特性的概要编写虚设代码(dummy code),不用准确实现功能,只要保证测试可以通过即可。

    (4)现在重写(Refactor)代码,真正实现需要的特性功能,之后要保证测试一直成功。

      (2)doctest模块

    函数、类或模块的第一行如果是一个字符串,这个字符串就是“文档字符串”。 使用doctest模块可以在文档字符串中查找“交互式会话”的例子, 并使用一系列测试的形式测试这些例子给出的数据和结果。

    # my_math.py
    def add2(x,y):
        """
        函数名:add(x,y)
        功能:返回2个输入参数之和
        >>> add2(1, 2)
        3
        >>> add2(1.3, 2.4)
        3.7
        """
        return x+y

    上例中,在自定义的add2()函数的文档字符串中,举了2个调用和结果的例子, doctest模块可以从my_math.py文件中提取出add2()的示例部分, 加以测试,若测试结果和示例结果不同,则会输出错误报告。

    可以编写单独的测试文件,也可以在库模块的末尾包含测试代码来测试自身,以下分别示例:

    ● 编写单独的测试文件:

    # my_math.py
    def add2(x,y):
        ...(内容同上例)
        
    # testmymath.py (单独测试文件)
    import my_math
    import doctest
    nfails, ntests = doctest.testmod(my_math)   # nfails和ntest分别表示失败的数量和执行的总测试数

    如果所有测试都顺利通过,则不产生输出。否则将会在屏幕上输出错误报告, 如果想在屏幕上看到测试的详细输出,可使用verbose参数:

    doctest.testmod(my_math, verbose=True)

    ● 在库模块的末尾包含测试代码:

    # my_math.py
    def add2(x,y):
        ...(内容同上例)
        
    if __name__=='__main__':
        import doctest, my_math
        doctest.testmod(my_math)

    如果my_math.py文件作为主程序在解释器中运行,就会运行文档测试。否则, 如果文件是由import加载的,测试将被忽略。 如果所有测试都顺利通过,则不产生输出。否则将会在屏幕上输出错误报告, 如果想在屏幕上看到测试的详细输出,可使用以下-v参数:

    $ python my_math.py -v

      (3)unittest模块

    对于更全面的程序测试,可以使用unittest模块。如果进行单元测试, 开发人员会为程序的每个组成元素(如各个函数、方法、类和模块)编写独立的测试案例。 然后运行这些测试来验证组成更大程序的基本组件的行为是否正确。

    unittest的基本使用方法为:定义一个继承自unittest.TestCase的类, 在这个类中,各种测试由以名称 test 开头的方法定义,在每隔测试内,可用各种断言来检查不同条件。

    TestCase实例支持以下方法和属性:

    实例方法说明
    t.setUp() 在运行任何测试方法之前,调用它来执行设置步骤。
    t.tearDown() 在运行测试之后,调用它来执行清除操作。
    t.assert_(expr [,msg]) 如果expr的计算结果为False,表明测试失败。 msg是一条消息字符串,提供对失败的解释。
    t.failUnless(expr [,msg])
    t.assertEqual(x, y, [,msg]) 如果x和y不相等,则表明测试失败。msg含义同上。
    t.failUnlessEqual(x, y, [,msg])
    t.assertNotEqual(x, y, [,msg]) 如果x和y相等,则表明测试失败。msg含义同上。
    t.failIfEqual(x, y, [,msg])
    t.assertAlmostEqual(x, y, [,places [,msg]]) 如果数字x和y未包含在对方的places小数位中,则表明测试失败。 检查方法是计算x和y的差,并将结果舍入到给定位数。如果结果为0,则x和y的值相近。 msg含义同上。
    t.failUnlessAlmostEqual(x, y, [,places [,msg]])
    t.assertNotAlmostEqual(x, y, [,places [,msg]]) 如果x和y在places小数位内无法区分大小,则表明测试失败。 msg含义同上。
    t.failIfAlmostEqual(x, y, [,places [,msg]])
    t.assertRaises(exc, callable, ...) 如果可调用对象callable未引发一场exc,则表明测试失败。 剩余参数将以参数形式传递给callable。可以使用异常元组exc检查多个异常。 msg含义同上。
    t.failUnlessRaises(exc, callable, ...)
    t.failIf(expr [,msg]) 如果expr计算结果为True,则表明测试失败。msg含义同上。
    t.fail([msg]) 表明测试失败,msg含义同上。
    t.failureException 该属性设置为在测试中捕获到的最后一个异常值。用于不仅要检查是否出现异常, 还想要检查异常是否抛出了恰当的值。

    ● 单元测试的例子

    如果需要编写单元测试来测试前面add2()函数的各个方面,可以创建一个独立模块 testmymath.py,如下例所示。

    import my_math
    import unittest
    
    # 单元测试
    class TestMyMathFunction(unittest.TestCase):
        def setUp(self):
            # 执行设置操作(如果有的话)
            pass
            
        def tearDown(self):
            # 执行清除操作(如果有的话)
            pass
            
        def testintint(self):
            r = my_math.add2(2,3)
            self.assertEqual(r, 5)
            
        def testfloatfloat(self):
            r = my_math.add2(1.2, 3.4)
            self.assertEqual(r, 4.6)
            
        def testintfloat(self):
            r = my_math.add2(2, 3.5)
            self.assertEqual(r, 5.5)
            
            
    # 运行unittest
    if __name__=='__main__':
        unittest.main()

    要运行单元测试,只需在文件 testmymath.py 上运行Python:

    $ python testmymath.py

      (4)调试器和pdb模块

    Python提供了一个基于命令的简单调试器,pdb模块支持:事后检查、检查栈帧、设置断点、单步调试以及计算代码。

    ● pdb模块的功能函数

    函数说明
    run(statement [,globals [,locals]]) 在调试器控制下执行statement,globals 和 locals 分别定义运行代码的全局和局部命名空间。
    runeval(expression [,globals [,locals]]) 在调试器控制下计算expression字符串表达式。成功运行之后, 将返回表达式的值。globals 和 locals 含义同上。
    runcall(function [,argument, ...]) 在调试器内调用一个函数。function是一个可调用对象, 其他参数可在其后的argument部分提供。函数运行后将返回它的返回值。
    set_trace() 在调用该函数的位置启动调试器,这可用于将调试器断点硬编码到程序代码中。
    post_mortem(traceback) 对回溯对象traceback启动事后检查。通常使用sys.exc_info()等函数获得。
    pm() 使用最后一个异常的回溯进入事检查调试。

    ● 从Python交互环境启动调试器

    启动调试器后,会显示一个(Pdb)提示符:

    以下为待调试文件

    #testfile.py
    def testadd(a, b):
        c = a + b;
        return c

    以下为交互环境命令行:

    >>> import pdb
    >>> import testfile   # 要调试的文件名
    >>> pdb.run('testfile.testadd(1,2)')
    > <string>(1)()
    (Pdb)

    ● 调试器命令

    可以在调试器提示符(Pdb)下使用命令,一些命令具有长短2种形式,例如, h(elp)表示hhelp都是可接受的。 可用调试命令见下表:

    命令说明
    [!]statement 在当前栈帧上下文中执行一行statement语句。感叹号可以忽略, 但如果语句的第一个词于调试器命令类似,则必须使用它来避免歧义。如要设置全局变, 可在同一行上添加global命令作为前缀,如:global a; a = 1
    a(rgs) 打印当前函数的参数列表。
    alias [name [command]] 创建名为name的别名来运行command。在command字符串中, 键入别名时子字符串'%1', '%2'等被替换为相应的参数,'%*'被替换为所有参数。 ,如果没有给定任何命令,则显示当前别名列表。command可以很长, 甚至可以使用for循环等(但需写在一行内)。
    b(reak) [loc [,condition]] 在位置loc处设置断点。loc指定一个特定文件名和行号, 或者指定一个模块中的一个函数名称,可使用以下语法:
        n(当前文件中的行号);
        filename:n(另一个文件中的行号);
        function(当前模块中的函数名称);
        module.function(某个模块中的函数名称);
    如果省略了loc,将打印当前的所有断点。condition是一个表达式, 在打印断点之前,该表达式的值必须计算为True。所有断点都会被分配一个数字, 该数字将在完成此命令时作为输出打印出来。这些数字可以在一些其他调试命令中使用。
    cl(ear) [bpnumber [bpnumber ...]] 清除断点编号列表。如果没指明断点编号,所有断点都会被清除。
    commands [bpnumber] 设置在遇到bpnumber时,将自动执行一系列调试命令。列出要执行的命令时, 只需在后续行中键入它们并使用end来标记命令序列的结束即可。 如果包含continue命令,在遇到断点时,程序将自动继续执行。如果省略bpnumber, 将使用最后一个断点集。
    condition bpnumber [condition] 在断点上放置一个条件。condition是一个表达式,在识别该断点之前, 该表达式的值必须计算为True。省略该条件会清除任何以前的条件。
    c(ont(inue)) 继续执行,直到遇到下一个断点。
    disable [bpnumber [bpnumber ...]] 禁用指定断点集,可以在以后用enable命令重新启用这些断点。
    d(own) 将当前帧在栈跟踪中下移一层。
    enable [bpnumber [bpnumber ...]] 启用指定的断点集。
    h(elp) [command] 显示可用命令的列表。指定一个命令将返回该命令的帮助信息。
    ignore bpnumber [count] 忽略一个断点count次。
    j(ump) lineno 设置要执行的下一行。只能用于同一执行帧中的不同语句之间移动。而且, 无法跳到某些语句中(如循环中的语句)
    l(ist) [first [,last]] 列出源代码。如果没有参数,该命令将列出当前行前后的共11行。如果使用一个参数, 它将列出该行前后的11行。如果使用2个参数,它将列出指定范围内的行。
    n(ext) 执行到当前函数中的下一行,与step命令的区别在于, next 会将调用整体函数视为一行执行,而step将进入那个函数执行一行。
    p expression 在当前上下文中计算表达式expression的值并打印其值。
    q(uit) 退出调试器。
    r(eturn) 持续运行,直到当前函数返回值。
    run [args] 重新启动程序,并使用args中的命令行参数作为 sys.args 的新设置。 所有断点和其他调试器设置都会被保留。
    s(tep) 执行一行源代码并停在被调用的函数内。
    tbreak [loc [,condition]] 设置一个临时断点,该断点将在第一次到达时删除。 loccondition用法同前。
    u(p) 将当前帧在跟踪中上移一层。
    unalias name 删除指定别名
    until 恢复执行,直至不再控制当前执行帧,或者直至到达一个比当前行号大的行号。 例如在循环中键入 until 命令,将执行循环中的所有语句,直至循环结束。
    w(here) 打印栈跟踪。

    ● 从命令行进行调试

    可以在命令行上调用调试调试某文件,在这种情况下,启动程序时调试器将自动启动。

    $ python -m pdb testfile.py

    如果用户的主目录或当前目录中包含.pdbrc文件,那么每次调试器启动时将执行该文件, 可以使用这种方法来指定要在调试器每次启动时执行的调试命令。

      (5)程序探查

    profilecProfile模块用于收集探查信息,两个模块的工作方式相同, 但cProfile速度更快且更先进。程序探查可以通过以下命令行方式调用:

    $ python -m cProfile testfile.py

    运行该命令后,会在屏幕上打印出性能统计信息。

    也可以在交互命令行中或程序代码中调用探查器(profiler),具体方法是使用run()函数, 其语法如下:

    import cProfile
    cProfile.run(command [,filename])

    探查器会使用exec语句执行command的内容,filename是输出报告要保存到的文件, 如果忽略该参数,报告将输出到标准输出。

    生成报告的部分说明如下:

    抬头说明
    primitive calls 非递归性函数调用的数量
    ncalls 调用总数(包括自递归),当表示为 n/m 时,n表示实际调用数量,m表示原始调用的数量。
    tottime 该函数消耗的时间(不含子函数)
    percall tototime / ncalls
    cumtime 函数消耗的总时间
    percall cumtime / (primitive calls)
    filename:lineno(function) 每个函数的位置和名称

    通常对于普通的探查分析,cProfile模块足够了。如果希望保存数据和进一步分析, 可使用pstats模块,它可以进一步分析cProfile模块输出的报告信息。

    >>> import pstats
    >>> p = pstats.Stats('result.profile')

      (6)调优与优化

    ● 程序运行时间测量

    (1)方法一:使用Linux的time命令

    可用于简单对长时间运行的Python程序进行计时:

    $ time python testfile.py

    (2)方法二:在程序中直接放入统计时间代码

    time模块的perf_counter()函数可得到本进程开始起到现在的总秒数, process_time()函数可得到本进程开始起到现在的进程运行秒数。

    import time
    start_proc = time.process_time()    # 进程时间
    start_real = time.time()            # UTC时间
    ......
    end_proc = time.process_time()
    end_real = time.time()
    print('%f Real Seconds" %(end_real - start_real))
    print('%f Process Seconds" %(end_proc - start_proc))
    

    (3)方法三:使用timeit()函数

    如果想对一个特定语句进行基准测试,可以使用timeit模块中的timeit()函数,语法如下:

    timeit(code [,setup])

    其中,code参数时希望对其基准测试的代码,setup参数是一条语句, 用来设置执行环境。timeit()函数会运行这条语句100万次并报告执行时间。 可以向timeit()函数提供number=count关键字参数来更改重复次数。

    >>> from timeit import timeit
    >>> timeit('math.sqrt(3.0)', 'import math')
    0.10910729999886826
    
    >>> timeit('sqrt(3.0)', 'from math import sqrt')
    0.0762049000004481

    timeit模块还有一个repeat()函数,功能与timeit()相同, 但它重复测量5次并返回一个结果列表:

    >>> from timeit import repeat
    >>> repeat('math.sqrt(3.0)', 'import math')
    [0.07255980000081763, 0.07150849999925413, 0.07361959999980172, 0.0723534999997355, 0.08293649999905028]

    ● 内存测量

    sys模块有一个getsizeof()函数,可用于分析Python对象的内存占用(以字节为单位):

    >>> import sys
    >>> sys.getsizeof(2)
    28
    >>> sys.getsizeof('a')
    50
    >>> sys.getsizeof([1,])
    72
    >>> sys.getsizeof([1,2,3])
    88
    >>> sum(sys.getsizeof(x) for x in [1,2,3])
    84

    对于列表、元组和字典等容器,报告的大小只是容器对象本身的大小,不是容器中包含的所有对象的累计大小。 可以像上面显示的那样使用sum()函数来计算列表内容的总大小。

    测量实际内存占用的一种辅助技术是,从操作系统的进程查看器或任务管理器检查正在运行的程序。

    ● 返汇编

    dis模块可用于将Python函数、方法、类反汇编为低级的解释器指令, 该模块中的dis()函数使用示例如下:

    >>> from dis import dis
    >>> import testfile
    >>> dis(testfile.testadd)

    在多线程程序中,反汇编中的每行操作都采用是原子执行方式,一行不会被中间打断, 可以利用此信息来跟踪复杂的竞争条件。

    ● 调优策略

    下面列出了一些比较典型的优化策略:

    (1)尽量使用内置类型

    Python内置的元组、列表、集合、字典完全是用C语言实现的,是解释器中优化程度最高的数据结构。 尽量避免构建自定义的数据结构(如:二叉搜索树、链表等)来模仿它们的功能。 标准库中的类型也是很好的选择,例如collection.deque双端队列,用它在队列头插入项, 比在普通列表头部插入项要高效得多。

    (2)能使用字典结构就不要定义新class

    用户定义得类和实例时用字典构建的,因此,查找、设置、删除实例数据的速度几乎总是比直接 在字典上执行这些操作更慢。如果只是构建一个简单的数据结构来存储数据,元组和字典通常就够用了。

    (3)使用__slots__

    如果程序创建了自定义类的大量实例,可以考虑在类定义中使用__slots__属性。 __slots__有时被看作一种安全功能,因为它会限制属性名称的设置, 但它更主要的用途是性能优化。使用__slots__的类不使用字典存储实例数据 (而是用一种更高效的内部数据结构),所以其实例使用的内存也更少、访问速度也更快。

    不过,需要注意的是,将__slots__功能添加到类中可能会无故破坏其他代码。 因为__slots__会占用__dict__属性,故依赖__dict__的代码会失败。

    (4)避免多次重复使用(.)运算符

    使用.在对象上查找属性时,总会涉及名称查找。对于大量使用方法或模块查找的计算, 最好首先将要执行的操作方道一个局部变量中,从而避免属性查找。例如:使用 from math import sqrt 和 sqrt(x) 要比 math.sqrt(x) 要快 1.4 倍左右。

    (5)使用异常来处理不常见的情况,但是避免对常见情况使用异常

    对于try中语句块正常运行的情况,其运行速度要比前面加一个额外的if判断要快10%左右, 但是如果经常会让程序陷入异常except的语句块,程序就会变得非常慢。 因此,在这种情况下,用if判断语句比较好。

    另外,检查字典中是否有某个键名,用in操作符也比用d.get(key)要快2倍。

    (6)鼓励使用函数式编程和迭代

    使用:列表推导、生成器表达式、生成器、协程、闭包,这些方式会使程序执行效率大大提高, 尤其是对于大量数据处理来说,更是如此。射你用生成器编写的代码,不仅运行速度快,而且内存使用效率也高。

    (7)使用装饰器和元类

    装饰器和元类用于修改函数和类,可以通过多种方式使用它们来改进性能, 特别是程序拥有很多可以启动或禁用的可选功能时。

     

    返回目录

  • 相关阅读:
    修改MySQL表中自增编号
    springboot整合mybatis统一配置bean的别名
    kotlin来了!!
    微信小程序获取登录手机号
    maven--package
    修改oracle数据库时间
    oracle启动停止命令
    安装 MySQL 之后初始密码在哪里??
    EntityFramework~~~三种模式
    webqq协议分析之~~~~验证是否需要验证码
  • 原文地址:https://www.cnblogs.com/initcircuit/p/12213179.html
Copyright © 2020-2023  润新知