Learning Python - Mark Lutz [285]
_reps = kargs.pop('_reps', 1000) # Passed-in or default reps
trace(func, pargs, kargs, _reps)
repslist = range(_reps) # Hoist range out for 2.6 lists
start = timefunc()
for i in repslist:
ret = func(*pargs, **kargs)
elapsed = timefunc() - start
return (elapsed, ret)
def best(func, *pargs, **kargs):
_reps = kargs.pop('_reps', 50)
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 module’s docstring at the top of the file describes its intended usage. It uses dictionary pop operations to remove the _reps argument from arguments intended for the test function and provide it with a default, and it traces arguments during development if you change its trace function to print. To test with this new timer module on either Python 3.0 or 2.6, change the timing script as follows (the omitted code in the test functions of this version use the x + 1 operation for each test, as coded in the prior section):
# File timeseqs.py
import sys, mytimer
reps = 10000
repslist = range(reps)
def forLoop(): ...
def listComp(): ...
def mapCall(): ...
def genExpr(): ...
def genFunc(): ...
print(sys.version)
for tester in (mytimer.timer, mytimer.best):
print('<%s>' % tester.__name__)
for test in (forLoop, listComp, mapCall, genExpr, genFunc):
elapsed, result = tester(test)
print ('-' * 35)
print ('%-9s: %.5f => [%s...%s]' %
(test.__name__, elapsed, result[0], result[-1]))
When run under Python 3.0, the timing results are essentially the same as before, and relatively the same for both the total-of-N and best-of-N timing techniques—running tests many times seems to do as good a job filtering out system load fluctuations as taking the best case, but the best-of-N scheme may be better when testing a long-running function. The results on my machine are as follows:
C:\misc> c:\python30\python timeseqs.py
3.0.1 (r301:69561, Feb 13 2009, 20:04:18) [MSC v.1500 32 bit (Intel)]
----------------------------------- forLoop : 2.35371 => [10...10009] ----------------------------------- listComp : 1.29640 => [10...10009] ----------------------------------- mapCall : 3.16556 => [10...10009] ----------------------------------- genExpr : 1.97440 => [10...10009] ----------------------------------- genFunc : 1.95072 => [10...10009] ----------------------------------- forLoop : 0.00193 => [10...10009] ----------------------------------- listComp : 0.00124 => [10...10009] ----------------------------------- mapCall : 0.00268 => [10...10009] ----------------------------------- genExpr : 0.00164 => [10...10009] ----------------------------------- genFunc : 0.00165 => [10...10009] The times reported by the best-of-N timer here are small, of course, but they might become significant if your program iterates many times over large data sets. At least in terms of relative performance, list comprehensions appear best in most cases; map is only slightly better when built-ins are applied. Using keyword-only arguments in 3.0 We can also make use of Python 3.0 keyword-only arguments here to simplify the timer module’s code. As we learned in Chapter 19, keyword-only arguments are ideal for configuration options such as our functions’ _reps argument. They must be coded after a * and before a ** in the function header, and in a function call they must be passed by keyword and appear before the ** if used. Here’s a keyword-only-based alternative to the prior module. Though simpler, it compiles and runs under Python 3.X only, not 2.6: # File mytimer.py (3.X only) """ Use 3.0 keyword-only default arguments, instead of ** and dict pops. No need to hoist range() out of test in 3.0: a generator, not a list """ import time, sys trace = lambda *args: None # or print timefunc = time.clock if sys.platform == 'win32' else time.time def timer(func, *pargs, _reps=1000, **kargs): trace(func, pargs, kargs, _reps) start = timefunc() for i in range(_reps): ret = func(*pargs, **kargs)