Learning Python - Mark Lutz [286]
elapsed = timefunc() - start
return (elapsed, ret)
def best(func, *pargs, _reps=50, **kargs):
best = 2 ** 32
for i in range(_reps):
(time, ret) = timer(func, *pargs, _reps=1, **kargs)
if time < best: best = time
return (best, ret)
This version is used the same way as and produces results identical to the prior version, not counting negligible test time differences from run to run:
C:\misc> c:\python30\python timeseqs.py
...same results as before...
In fact, for variety we can also test this version of the module from the interactive prompt, completely independent of the sequence timer script—it’s a general-purpose tool:
C:\misc> c:\python30\python
>>> from mytimer import timer, best
>>>
>>> def power(X, Y): return X ** Y # Test function
...
>>> timer(power, 2, 32) # Total time, last result
(0.002625403507987747, 4294967296)
>>> timer(power, 2, 32, _reps=1000000) # Override defult reps
(1.1822605247314932, 4294967296)
>>> timer(power, 2, 100000)[0] # 2 ** 100,000 tot time @1,000 reps
2.2496919999608878
>>> best(power, 2, 32) # Best time, last result
(5.58730229727189e-06, 4294967296)
>>> best(power, 2, 100000)[0] # 2 ** 100,000 best time
0.0019937589833460834
>>> best(power, 2, 100000, _reps=500)[0] # Override default reps
0.0019845399345541637
For trivial functions like the one tested in this interactive session, the costs of the timer’s code are probably as significant as those of the timed function, so you should not take timer results too absolutely (we are timing more than just X ** Y here). The timer’s results can help you judge relative speeds of coding alternatives, though, and may be more meaningful for longer-running operations like the following—calculating 2 to the power one million takes an order of magnitude (power of 10) longer than the preceding 2**100,000:
>>> timer(power, 2, 1000000, _reps=1)[0] # 2 ** 1,000,000: total time
0.088112804839710179
>>> timer(power, 2, 1000000, _reps=10)[0]
0.40922470593329763
>>> best(power, 2, 1000000, _reps=1)[0] # 2 ** 1,000,000: best time
0.086550036387279761
>>> best(power, 2, 1000000, _reps=10)[0] # 10 is sometimes as good as 50
0.029616752967200455
>>> best(power, 2, 1000000, _reps=50)[0] # Best resolution
0.029486918030102061
Again, although the times measured here are small, the differences can be significant in programs that compute powers often.
See Chapter 19 for more on keyword-only arguments in 3.0; they can simplify code for configurable tools like this one but are not backward compatible with 2.X Pythons. If you want to compare 2.X and 3.X speed, for example, or support programmers using either Python line, the prior version is likely a better choice. If you’re using Python 2.6, the above session runs the same with the prior version of the timer module.
Other Suggestions
For more insight, try modifying the repetition counts used by these modules, or explore the alternative timeit module in Python’s standard library, which automates timing of code, supports command-line usage modes, and finesses some platform-specific issues. Python’s manuals document its use.
You might also want to look at the profile standard library module for a complete source code profiler tool—we’ll learn more about it in Chapter 35 in the context of development tools for large projects. In general, you should profile code to isolate bottlenecks before recoding and timing alternatives as we’ve done here.
It might be useful to experiment with the new str.format method in Python 2.6 and 3.0 instead of the % formatting expression (which could potentially be deprecated in the future!), by changing the timing script’s formatted print lines as follows:
print('<%s>' % tester.__name__) # From expression
print('<{0}>'.format(tester.__name__)) # To method call
print ('%-9s: %.5f => [%s...%s]' %
(test.__name__, elapsed, result[0], result[-1]))
print('{0:<9}: {1:.5f} => [{2}...{3}]'.format(
test.__name__, elapsed, result[0], result[-1]))
You can judge the difference between these techniques yourself.
You might try modifying