Learning Python - Mark Lutz [533]
class tracer:
def __init__(self, func): # On @ decoration: save original func
self.calls = 0
self.func = func
def __call__(self, *args): # On later calls: run original func
self.calls += 1
print('call %s to %s' % (self.calls, self.func.__name__))
self.func(*args)
@tracer
def spam(a, b, c): # spam = tracer(spam)
print(a + b + c) # Wraps spam in a decorator object
Notice how each function decorated with this class will create a new instance, with its own saved function object and calls counter. Also observe how the *args argument syntax is used to pack and unpack arbitrarily many passed-in arguments. This generality enables this decorator to be used to wrap any function with any number of arguments (this version doesn’t yet work on class methods, but we’ll fix that later in this section).
Now, if we import this module’s function and test it interactively, we get the following sort of behavior—each call generates a trace message initially, because the decorator class intercepts it. This code runs under both Python 2.6 and 3.0, as does all code in this chapter unless otherwise noted:
>>> from decorator1 import spam
>>> spam(1, 2, 3) # Really calls the tracer wrapper object
call 1 to spam
6
>>> spam('a', 'b', 'c') # Invokes __call__ in class
call 2 to spam
abc
>>> spam.calls # Number calls in wrapper state information
2
>>> spam
When run, the tracer class saves away the decorated function, and intercepts later calls to it, in order to add a layer of logic that counts and prints each call. Notice how the total number of calls shows up as an attribute of the decorated function—spam is really an instance of the tracer class when decorated (a finding that may have ramifications for programs that do type checking, but is generally benign). For function calls, the @ decoration syntax can be more convenient than modifying each call to account for the extra logic level, and it avoids accidentally calling the original function directly. Consider a nondecorator equivalent such as the following: calls = 0 def tracer(func, *args): global calls calls += 1 print('call %s to %s' % (calls, func.__name__)) func(*args) def spam(a, b, c): print(a, b, c) >>> spam(1, 2, 3) # Normal non-traced call: accidental? 1 2 3 >>> tracer(spam, 1, 2, 3) # Special traced call without decorators call 1 to spam 1 2 3 This alternative can be used on any function without the special @ syntax, but unlike the decorator version, it requires extra syntax at every place where the function is called in your code; furthermore, its intent may not be as obvious, and it does not ensure that the extra layer will be invoked for normal calls. Although decorators are never required (we can always rebind names manually), they are often the most convenient option. State Information Retention Options The last example of the prior section raises an important issue. Function decorators have a variety of options for retaining state information provided at decoration time, for use during the actual function call. They generally need to support multiple decorated objects and multiple calls, but there are a number of ways to implement these goals: instance attributes, global variables, nonlocal variables, and function attributes can all be used for retaining state. Class instance attributes For example, here is an augmented version of the prior example, which adds support for keyword arguments and returns the wrapped function’s result to support more use cases: class tracer: # State via instance attributes def __init__(self, func): # On @ decorator self.calls = 0 # Save func for later call self.func = func def __call__(self, *args, **kwargs): # On call to original function self.calls += 1 print('call %s to %s' % (self.calls, self.func.__name__)) return self.func(*args, **kwargs) @tracer def spam(a, b, c): # Same as: spam = tracer(spam) print(a + b + c) # Triggers tracer.__init__ @tracer def eggs(x, y): # Same