Learning Python - Mark Lutz [538]
In the rest of this chapter we’re going to be fairly casual about using classes or functions to code our function decorators, as long as they are applied only to functions. Some decorators may not require the instance of the original class, and will still work on both functions and methods if coded as a class—something like Python’s own staticmethod decorator, for example, wouldn’t require an instance of the subject class (indeed, its whole point is to remove the instance from the call).
The moral of this story, though, is that if you want your decorators to work on both simple functions and class methods, you’re better off using the nested-function-based coding pattern outlined here instead of a class with call interception.
Timing Calls
To sample the fuller flavor of what function decorators are capable of, let’s turn to a different use case. Our next decorator times calls made to a decorated function—both the time for one call, and the total time among all calls. The decorator is applied to two functions, in order to compare the time requirements of list comprehensions and the map built-in call (for comparison, also see Chapter 20 for another nondecorator example that times iteration alternatives like these):
import time
class timer:
def __init__(self, func):
self.func = func
self.alltime = 0
def __call__(self, *args, **kargs):
start = time.clock()
result = self.func(*args, **kargs)
elapsed = time.clock() - start
self.alltime += elapsed
print('%s: %.5f, %.5f' % (self.func.__name__, elapsed, self.alltime))
return result
@timer
def listcomp(N):
return [x * 2 for x in range(N)]
@timer
def mapcall(N):
return map((lambda x: x * 2), range(N))
result = listcomp(5) # Time for this call, all calls, return value
listcomp(50000)
listcomp(500000)
listcomp(1000000)
print(result)
print('allTime = %s' % listcomp.alltime) # Total time for all listcomp calls
print('')
result = mapcall(5)
mapcall(50000)
mapcall(500000)
mapcall(1000000)
print(result)
print('allTime = %s' % mapcall.alltime) # Total time for all mapcall calls
print('map/comp = %s' % round(mapcall.alltime / listcomp.alltime, 3))
In this case, a nondecorator approach would allow the subject functions to be used with or without timing, but it would also complicate the call signature when timing is desired (we’d need to add code at every call instead of once at the def), and there would be no direct way to guarantee that all list builder calls in a program are routed through timer logic, short of finding and potentially changing them all.
When run in Python 2.6, the output of this file’s self-test code is as follows:
listcomp: 0.00002, 0.00002
listcomp: 0.00910, 0.00912
listcomp: 0.09105, 0.10017
listcomp: 0.17605, 0.27622
[0, 2, 4, 6, 8]
allTime = 0.276223304917
mapcall: 0.00003, 0.00003
mapcall: 0.01363, 0.01366
mapcall: 0.13579, 0.14945
mapcall: 0.27648, 0.42593
[0, 2, 4, 6, 8]
allTime = 0.425933533452
map/comp = 1.542
Testing subtlety: I didn’t run this under Python 3.0 because, as described in Chapter 14, the map built-in returns an iterator in 3.0, instead of an actual list as in 2.6. Hence, 3.0’s map doesn’t quite compare directly to a list comprehension’s work (as is, the map test takes virtually no time at all in 3.0!).
If you wish to run this under 3.0, too, use list(map()) to force it to build a list like the list comprehension does, or else you’re not really comparing apples to apples. Don’t do so in 2.6, though—if you do, the map test will be charged for building two lists, not one.
The following sort of code would pick fairly for 2.6 and 3.0; note, though, that while this makes the comparison between list comprehensions and map more fair in either 2.6 or 3.0, because range is also an iterator in 3.0, the results for 2.6 and 3.0 won’t compare directly:
...
import sys
@timer
def listcomp(N):
return [x * 2 for x in range(N)]
if sys.version_info[0] == 2:
@timer
def mapcall(N):