当找到慢速函数时,有时需要进行更多的测试工作,只测试程序的一部分。通过在速
度测试中手动检测一部分代码来完成测试。
例如,可以从装饰器使用 cProfile 模块,如下所示:
import tempfile, os, cProfile, pstats
def profile(column=‘time’, list=5):
… def _profile(function):
… def __profile(*args, **kw):
… s = tempfile.mktemp()
… profiler = cProfile.Profile()
… profiler.runcall(function, args, **kw)
… profiler.dump_stats(s)
… p = pstats.Stats(s)
… p.sort_stats(column).print_stats(list)
… return __profile
… return profile
…
from myapp import main
@profile(‘time’, 6)
… def main_profiled():
… return main()
…
main_profiled()
Mon Apr 4 22:01:01 2016 /tmp/tmpvswuovz
1207 function calls in 8.243 seconds
Ordered by: internal time
List reduced from 7 to 6 due to restriction <6>
Ncalls tottime percall cumtime percall file:lineno(function)
602 8.241 0.014 8.241 0.014 {built-in method sleep}
400 0.001 0.000 4.026 0.010 myapp.py:5(medium)
2 0.001 0.000 8.243 4.121 myapp.py:13(heavy)
200 0.000 0.000 0.213 0.001 myapp.py:9(light)
1 0.000 0.000 8.243 8.243 myapp.py:21(main)
1 0.000 0.000 8.243 8.243 :1(main_profiled)
from myapp import light
stats = profile()(light)
stats()
Mon Apr 4 22:01:57 2016 /tmp/tmpnp_zk7dl
3 function calls in 0.001 seconds
Ordered by: internal time
ncalls tottime percall cumtime percall file:lineno(function)
1 0.001 0.001 0.001 0.001 {built-in method sleep}
1 0.000 0.000 0.001 0.001 myapp.py:9(light)
这种方法允许测试应用程序的一部分,并锐化统计输出。但在这个阶段,有一个被调
用者列表可能不是很有用,因为已经指出该函数需要进行优化。唯一令人关注的信息是知
道它有多快,然后增强它。
timeit 提供一种简单的方法来测量小代码片段的执行时间,它使用主机系统提供的
最佳底层计时器(time.time 或 time.clock),从而更好地满足这种需要,如下所示:
from myapp import light
import timeit
t = timeit.Timer(‘main()’)
t.timeit(number=5)
10000000 loops, best of 3: 0.0269 usec per loop
10000000 loops, best of 3: 0.0268 usec per loop
10000000 loops, best of 3: 0.0269 usec per loop
10000000 loops, best of 3: 0.0268 usec per loop
10000000 loops, best of 3: 0.0269 usec per loop
5.6196951866149902
该模块可以被重复调用,并且面向测试隔离的代码片段。这在应用程序上下文之外非常有用,在命令提示符中,例如,在现有应用程序中使用不是很方便。
但是应该谨慎使用 timeit 的结果。这是一个非常好的工具,它可以客观地比较两段
短代码,但它也会让你很容易犯下危险的错误,这将导致令人困惑的结论。这里,例如,
通过 timeit 模块对两个无害的代码片段进行比较,它可以使你认为通过加法的字符串连
接比 str.join()方法更快:
$ python3 -m timeit -s ‘a = map(str, range(1000))’ ‘“”.join(a)’
1000000 loops, best of 3: 0.497 usec per loop
$ python3 -m timeit -s ‘a = map(str, range(1000)); s=“”’ ‘for i in a: s
+= i’
10000000 loops, best of 3: 0.0808 usec per loop
从第 2 章开始,我们知道通过加法连接字符串不是一个好的模式。尽管有一些针对这
种用法设计的一些较小的 CPython 微优化,它最终会导致二次的运行时间。问题在于
timeit(命令行中-s 参数)设置参数的细微差别,以及 Python 3 中的范围如何工作。我
不会讨论问题的细节,但会把它留给你作为一个练习。总之,在 Python 3 中,这是正确的
比较使用加法和 str.join()连接字符串的方法,如下所示:
$ python3 -m timeit -s ‘a = [str(i) for i in range(10000)]’ ‘s=“”.
join(a)’
10000 loops, best of 3: 128 usec per loop
$ python3 -m timeit -s ‘a = [str(i) for i in range(10000)]’ ’
s = “”
for i in a:
s += i
’
1000 loops, best of 3: 1.38 msec per loop
测量 Pystones
当测量执行时间时,结果取决于计算机硬件。为了能够产生通用测量,最简单的方法
测量固定序列的代码基准速度,并计算出它的比率。由此,函数所花费的时间可以转换为
一个比较通用的值,可以在任何计算机上比较。
Python 在其 test 包中提供了一个基准测试工具,用于测量一个精选的操作序列的持
续时间。结果是每秒钟计算机能够执行的 pystones 数量,在现代硬件上通常约为一秒,
如下所示:
from test import pystone
pystone.pystones()
(1.0500000000000007, 47619.047619047589)
该比率可用于将分析的持续时间转换为 pystones 的数量:
from test import pystone
benchtime, pystones = pystone.pystones()
def seconds_to_kpystones(seconds):
… return (pystonesseconds) / 1000
…
…
seconds_to_kpystones(0.03)
1.4563106796116512
seconds_to_kpystones(1)
48.543689320388381
seconds_to_kpystones(2)
97.087378640776762
seconds_to_kpystones 返回千个 ρystones 的数量。如果你想编写一些速度断言,
这个转换可以包含在你的测试中。
拥有 pystones 将允许你在测试中使用此装饰器,以便于你在执行时间上设置断言。
这些测试可以在任何计算机上运行,并让开发人员避免速度回归。当应用程序的一部分已
优化时,他们将能够在测试中设置其最大执行时间,并确保它不会被进一步的更改所破坏。
这种方法当然不是理想的并且 100%准确的,但是它至少比以硬编码的执行时间断言更好,
这些断言是以秒表示的原始值。