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)表示h和help都是可接受的。 可用调试命令见下表:
| 命令 | 说明 |
|---|---|
| [!]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]] | 设置一个临时断点,该断点将在第一次到达时删除。loc和condition用法同前。
|
| u(p) | 将当前帧在跟踪中上移一层。 |
| unalias name | 删除指定别名 |
| until |
恢复执行,直至不再控制当前执行帧,或者直至到达一个比当前行号大的行号。 例如在循环中键入 until 命令,将执行循环中的所有语句,直至循环结束。 |
| w(here) | 打印栈跟踪。 |
● 从命令行进行调试
可以在命令行上调用调试调试某文件,在这种情况下,启动程序时调试器将自动启动。
$ python -m pdb testfile.py
如果用户的主目录或当前目录中包含.pdbrc文件,那么每次调试器启动时将执行该文件, 可以使用这种方法来指定要在调试器每次启动时执行的调试命令。
(5)程序探查
profile和cProfile模块用于收集探查信息,两个模块的工作方式相同, 但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)使用装饰器和元类
装饰器和元类用于修改函数和类,可以通过多种方式使用它们来改进性能, 特别是程序拥有很多可以启动或禁用的可选功能时。
Python语法速查: 14. 测试与调优的更多相关文章
- Python语法速查:目录
1. 数据类型与内置函数 2. 列表.元组.字典.集合操作 3. 字符串格式化 4. 字符串常用操作 5. 运算符.math模块.表达式 6. 循环与迭代 7. 函数基础 8. 类与对象 9. 函数进 ...
- Python语法速查: 4. 字符串常用操作
返回目录 (1)字符串常用方法 Python3中,字符串全都用Unicode形式,所以省去了很多以前各种转换与声明的麻烦.字符串属于序列,所有序列可用的方法(比如切片等)都可用于字符串. 注意:字符串 ...
- Python语法速查: 7. 函数基础
返回目录 (1)函数基本 ● 函数是第一类对象 Python中万物皆对象,所有对象都是第一类的(first class),函数也不例外,也是第一类对象.既然是对象,那就可以当作普通的对象数据处理,比如 ...
- Python语法速查: 12. 文件与输入输出
返回目录 (1)文件基本操作 ● 文件常用操作 内置函数或方法 描述 open(name [,mode [,buffering]]) 内置函数.用来打开文件,返回一个文件对象(file对象).详见下述 ...
- Python语法速查: 1. 数据类型与内置函数
返回目录 (1)常用内置数据类型 分类 类型名称 描述 数字 int 整数 float 浮点数 complex 复数 bool 布尔值 序列 str 字符串(不可变序列) list 列表 tuple ...
- Python语法速查: 3. 字符串格式化
返回目录 (1)简易字符串格式化 字符串属于不可变序列,只能生成新的,不能改变旧的.“字符串格式化”有点像以前C语言的sprintf,可以将若干变量代入格式化的字符串,生成一个符合要求的新字符串. 转 ...
- Python语法速查: 5. 运算符、math模块、表达式
返回目录 (1)一些较容易搞错的运算符 一般简单的如加减乘除之类的运算符就不写了,这里主要列些一些容易搞错或忘记的运算符.运算符不仅仅只有号,有一些英文单词如 in, and 之类,也是运算符,并不是 ...
- Python语法速查: 20. 线程与并发
返回目录 本篇索引 (1)线程基本概念 (2)threading模块 (3)线程间同步原语资源 (4)queue (1)线程基本概念 当应用程序需要并发执行多个任务时,可以使用线程.多个线程(thre ...
- Python语法速查: 13. 操作系统服务
返回目录 本篇索引 (1)sys模块 (2)os模块 (3)与Windows相关模块 (4)subprocess模块 (5)signal模块 (1)sys模块 sys模块用于Python解释器及其环境 ...
随机推荐
- CCF_201604-3_路径解析
没有用stack来写,直接用了字符串和指针,过程有点复杂的.首先对读入的每一个路径,判断它是绝对路径或者相对路径,然后确定起始的路径,然后继续一位位的判断,"//","/ ...
- BZOJ 1009 [HNOI2008]GT考试(矩阵快速幂优化DP+KMP)
题意: 求长度为n的不含长为m的指定子串的字符串的个数 1s, n<=1e9, m<=50 思路: 长见识了.. 设那个指定子串为s f[i][j]表示长度为i的字符串(其中后j个字符与s ...
- EMC networker nmm can restore and recover sqlserver as different name to different location
EMC networker nmm can restore and recover sqlserver as different name to different location That is ...
- SpringBoot之ApplicationRunner接口和@Order注解
我们在开发中可能会有这样的情景.需要在容器启动的时候执行一些内容.比如读取配置文件,数据库连接之类的.SpringBoot给我们提供了ApplicationRunner接口来帮助我们实现这种需求.该接 ...
- bootstrap 表单类
bootstrap 表单类 表单美化 用法: 向父元素 <form role="form"></form> 添加 role="form&q ...
- asp.net core系列 WebAPI 作者:懒懒的程序员一枚
asp.net core系列 36 WebAPI 搭建详细示例一.概述1.1 创建web项目1.2 添加模型类1.3 添加数据库上下文1.4 注册上下文1.5 添加控制器1.6 添加Get方法1.7 ...
- 最简单的windows 10 软路由
因为轻信了 小米路由器3潘多拉固件刷机教程 年前把自己的小米路由器3pro 刷程砖了,然后自己有一台 i5256 的三众小主机,连在电信光猫上,可以拨号,勉强可以用,but 家里的设备那么多尤其手机笔 ...
- 渡一教育公开课重点笔记之css
主流浏览器及内核 浏览器 内核 IE trident Firefox Gecko Google chrome Webkit/blink(2014年上 ...
- C语言实现matlab的interp2()函数
项目要用到matlab中的Vq = interp2(X,Y,V,Xq,Yq)函数,即把一个已知经纬度和对应值的矩阵,插值变换到一个给定经纬度网格中,也就是对给定网格填值,需要用到插值,这里使用双线性内 ...
- 入侵检测基本准则(Basic principles of intrusion detection)【v1.0】
所谓“入侵检测”,顾名思义,就是对入侵行为的发觉.他通过对计算机网络或计算机系统中若干关键点收集信息并对其进行分析,从中发现网络或系统中是否有违反安全策略的行为和被攻击的迹象.” 但实际上,所谓的“违 ...