Learning Python - Mark Lutz [536]
Unfortunately, when our decorated method name is rebound to a class instance object with a __call__, Python passes only the tracer instance to self; it doesn’t pass along the Person subject in the arguments list at all. Moreover, because the tracer knows nothing about the Person instance we are trying to process with method calls, there’s no way to create a bound method with an instance, and thus no way to correctly dispatch the call.
In fact, the prior listing winds up passing too few arguments to the decorated method, and results in an error. Add a line to the decorator’s __call__ to print all its arguments to verify this; as you can see, self is the tracer, and the Person instance is entirely absent:
<__main__.tracer object at 0x02D6AD90> (0.25,) {}
call 1 to giveRaise
Traceback (most recent call last):
File "C:/misc/tracer.py", line 56, in bob.giveRaise(.25) File "C:/misc/tracer.py", line 9, in __call__ return self.func(*args, **kwargs) TypeError: giveRaise() takes exactly 2 positional arguments (1 given) As mentioned earlier, this happens because Python passes the implied subject instance to self when a method name is bound to a simple function only; when it is an instance of a callable class, that class’s instance is passed instead. Technically, Python only makes a bound method object containing the subject instance when the method is a simple function. Using nested functions to decorate methods If you want your function decorators to work on both simple functions and class methods, the most straightforward solution lies in using one of the other state retention solutions described earlier—code your function decorator as nested defs, so that you don’t depend on a single self instance argument to be both the wrapper class instance and the subject class instance. The following alternative applies this fix using Python 3.0 nonlocals. Because decorated methods are rebound to simple functions instead of instance objects, Python correctly passes the Person object as the first argument, and the decorator propagates it on in the first item of *args to the self argument of the real, decorated methods: # A decorator for both functions and methods def tracer(func): # Use function, not class with __call__ calls = 0 # Else "self" is decorator instance only! def onCall(*args, **kwargs): nonlocal calls calls += 1 print('call %s to %s' % (calls, func.__name__)) return func(*args, **kwargs) return onCall # Applies to simple functions @tracer def spam(a, b, c): # spam = tracer(spam) print(a + b + c) # onCall remembers spam spam(1, 2, 3) # Runs onCall(1, 2, 3) spam(a=4, b=5, c=6) # Applies to class method functions too! class Person: def __init__(self, name, pay): self.name = name self.pay = pay @tracer def giveRaise(self, percent): # giveRaise = tracer(giverRaise) self.pay *= (1.0 + percent) # onCall remembers giveRaise @tracer def lastName(self): # lastName = tracer(lastName) return self.name.split()[-1] print('methods...') bob = Person('Bob Smith', 50000) sue = Person('Sue Jones', 100000) print(bob.name, sue.name) sue.giveRaise(.10) # Runs onCall(sue, .10) print(sue.pay) print(bob.lastName(), sue.lastName()) # Runs onCall(bob), lastName in scopes This version works the same on both functions and methods: call 1 to spam 6 call 2 to spam 15 methods... Bob Smith Sue Jones call 1 to giveRaise 110000.0 call 1 to lastName call 2 to lastName Smith Jones Using descriptors to decorate methods Although the nested function solution illustrated in the prior section is the most straightforward way to support decorators that apply to both functions and class methods, other schemes are possible. The descriptor feature we explored in the prior chapter, for example, can help here as well. Recall