python函数性能测试的简单介绍( 四 )


正如我们已经看到的,Python的测试模块易于使用,并且极为有用 , 在我们使用 TDD的情况下更是如此 。它们还有比这里展示的要多得多的大量功能与特征——比如,跳过测试的能力 , 这有助于理解平台差别——并且这些都有很好的文档支持 。缺失的一个功能——但nose与py.test提供了——是测试发现,尽管这一特征被期望在后续的Python版本(或许与Python 3.2—起)中出现 。
性能剖析(Profiling)
如果程序运行很慢,或者消耗了比预期内要多得多的内存,那么问题通常是选择的算法或数据结构不合适,或者是以低效的方式进行实现 。不管问题的原因是什么, 最好的方法都是准确地找到问题发生的地方,而不只是检査代码并试图对其进行优化 。随机优化会导致引入bug,或者对程序中本来对程序整体性能并没有实际影响的部分进行提速,而这并非解释器耗费大部分时间的地方 。
在深入讨论profiling之前 , 注意一些易于学习和使用的Python程序设计习惯是有意义的,并且对提高程序性能不无裨益 。这些技术都不是特定于某个Python版本的,而是合理的Python程序设计风格 。第一 , 在需要只读序列时,最好使用元组而非列表; 第二,使用生成器,而不是创建大的元组和列表并在其上进行迭代处理;第三 , 尽量使用Python内置的数据结构 dicts、lists、tuples 而不实现自己的自定义结构,因为内置的数据结构都是经过了高度优化的;第四,从小字符串中产生大字符串时,不要对小字符串进行连接,而是在列表中累积,最后将字符串列表结合成为一个单独的字符串;第五,也是最后一点,如果某个对象(包括函数或方法)需要多次使用属性进行访问(比如访问模块中的某个函数) , 或从某个数据结构中进行访问,那么较好的做法是创建并使用一个局部变量来访问该对象,以便提供更快的访问速度 。
Python标准库提供了两个特别有用的模块 , 可以辅助调査代码的性能问题 。一个是timeit模块——该模块可用于对一小段Python代码进行计时,并可用于诸如对两个或多个特定函数或方法的性能进行比较等场合 。另一个是cProfile模块,可用于profile 程序的性能——该模块对调用计数与次数进行了详细分解,以便发现性能瓶颈所在 。
为了解timeit模块,我们将查看一些小实例 。假定有3个函数function_a()、 function_b()、function_c(), 3个函数执行同样的计算 , 但分别使用不同的算法 。如果将这些函数放于同一个模块中(或分别导入),就可以使用timeit模块对其进行运行和比较 。下面给出的是模块最后使用的代码:
if __name__ == "__main__":
repeats = 1000
for function in ("function_a", "function_b", "function_c"):
t = timeit.Timer("{0}(X, Y)".format(function),"from __main__ import {0}, X, Y".format(function))
sec = t.timeit(repeats) / repeats
print("{function}() {sec:.6f} sec".format(**locals()))
赋予timeit.Timer()构造子的第一个参数是我们想要执行并计时的代码,其形式是字符串 。这里,该字符串是“function_a(X,Y)”;第二个参数是可选的,还是一个待执行的字符串,这一次是在待计时的代码之前 , 以便提供一些建立工作 。这里,我们从 __main__ (即this)模块导入了待测试的函数,还有两个作为输入数据传入的变量(X 与Y),这两个变量在该模块中是作为全局变量提供的 。我们也可以很轻易地像从其他模块中导入数据一样来进行导入操作 。
调用timeit.Timer对象的timeit()方法时,首先将执行构造子的第二个参数(如果有), 之后执行构造子的第一个参数并对其执行时间进行计时 。timeit.Timer.timeit()方法的返回值是以秒计数的时间,类型是float 。默认情况下,timeit()方法重复100万次,并返回所 有这些执行的总秒数,但在这一特定案例中 , 只需要1000次反复就可以给出有用的结果, 因此对重复计数次数进行了显式指定 。在对每个函数进行计时后 , 使用重复次数对总数进行除法操作,就得到了平均执行时间 , 并在控制台中打印出函数名与执行时间 。

推荐阅读