Learning Python - Mark Lutz [529]
In more typical use, to insert logic that intercepts later calls to a function, we might code a decorator to return a different object than the original function:
def decorator(F):
# Save or use function F
# Return a different callable: nested def, class with __call__, etc.
@decorator
def func(): ... # func = decorator(func)
This decorator is invoked at decoration time, and the callable it returns is invoked when the original function name is later called. The decorator itself receives the decorated function; the callable returned receives whatever arguments are later passed to the decorated function’s name. This works the same for class methods: the implied instance object simply shows up in the first argument of the returned callable.
In skeleton terms, here’s one common coding pattern that captures this idea—the decorator returns a wrapper that retains the original function in an enclosing scope:
def decorator(F): # On @ decoration
def wrapper(*args): # On wrapped function call
# Use F and args
# F(*args) calls original function
return wrapper
@decorator # func = decorator(func)
def func(x, y): # func is passed to decorator's F
...
func(6, 7) # 6, 7 are passed to wrapper's *args
When the name func is later called, it really invokes the wrapper function returned by decorator; the wrapper function can then run the original func because it is still available in an enclosing scope. When coded this way, each decorated function produces a new scope to retain state.
To do the same with classes, we can overload the call operation and use instance attributes instead of enclosing scopes:
class decorator:
def __init__(self, func): # On @ decoration
self.func = func
def __call__(self, *args): # On wrapped function call
# Use self.func and args
# self.func(*args) calls original function
@decorator
def func(x, y): # func = decorator(func)
... # func is passed to __init__
func(6, 7) # 6, 7 are passed to __call__'s *args
When the name func is later called now, it really invokes the __call__ operator overloading method of the instance created by decorator; the __call__ method can then run the original func because it is still available in an instance attribute. When coded this way, each decorated function produces a new instance to retain state.
Supporting method decoration
One subtle point about the prior class-based coding is that while it works to intercept simple function calls, it does not quite work when applied to class method functions:
class decorator:
def __init__(self, func): # func is method without instance
self.func = func
def __call__(self, *args): # self is decorator instance
# self.func(*args) fails! # C instance not in args!
class C:
@decorator
def method(self, x, y): # method = decorator(method)
... # Rebound to decorator instance
When coded this way, the decorated method is rebound to an instance of the decorator class, instead of a simple function.
The problem with this is that the self in the decorator’s __call__ receives the decorator class instance when the method is later run, and the instance of class C is never included in *args. This makes it impossible to dispatch the call to the original method—the decorator object retains the original method function, but it has no instance to pass to it.
To support both functions and methods, the nested function alternative works better:
def decorator(F): # F is func or method without instance
def wrapper(*args): # class instance in args[0] for method
# F(*args) runs func or method
return wrapper
@decorator
def func(x, y): # func = decorator(func)
...
func(6, 7) # Really calls wrapper(6, 7)
class C:
@decorator
def method(self, x, y): # method = decorator(method)
... # Rebound to simple function
X = C()
X.method(6, 7) # Really calls wrapper(X, 6, 7)
When coded this way wrapper receives the C class instance in its first argument, so it can dispatch to the original method and access state information.
Technically,