Last time I began exploring Python decorators, which are a way of having one function “wrap” another function. Because the wrapper has access to both the input parameters and the return value, it can modify these values (unbeknownst to the inner function).
This time I pick up where I left off by exploring decorators modifying return values, decorators that take parameters, and decorators in classes.
To review, in Python, a decorator function is a regular function definition with the following properties:
- A decorator function takes one parameter, what it will wrap.
- A decorator function returns a function, the wrapper function.
The wrapper function returned by the decorator function must be compatible with whatever it wraps — almost always multiple different functions. For example, the
UpperCase decorator function from last time can be applied to any number of other functions (so long as they take a single string parameter).
So far we’ve modified the input arguments. Now let’s modify the return value.
Here’s the general pattern we use in Python for a decorator function:
def MyDecorator (inner_function): def wrapper (params): # ... modify params ... return inner_function(params) return wrapper @MyDecorator def some_function (params): # ... do function stuff ... return retval #
The decorator function,
MyDecorator, takes a parameter,
inner_function (the function to be wrapped), defines and returns a function,
The wrapper function receives the same input parameters as what it wraps, and it can modify those parameters before passing them to the wrapped function.
Note that, as written, the
wrapper function just returns whatever
inner_function returns, so it has no need to know what the return value is.
If we want to modify the return value, then we just make a slight change to the pattern:
def MyDecorator (inner_function): def wrapper (params): # ... modify params ... retval = inner_function(params) # ... modify retval ... return retval return wrapper #
Which, of course, assumes we know what to expect the wrapped function to return.
Bottom line, a “wrapper” function is just that; it wraps another function and has access to that function’s inputs and outputs.
We can parameterize the decorator function. This requires adding another function layer.
For example, suppose we had a decorator that encodes the input strings to functions. We want to pass an encoding key to the decorator because the keys can change or differ.
What we want is a decorator,
Encode, that lets us do this:
@Encode(key1) def function_1 (params): # ... do function stuff ... return retval @Encode(key2) def function_2 (params): # ... do function stuff ... return retval #
Encode decorator takes a parameter, the key to use for encoding. That key has to be passed to the decorator. Which seems like it might be a problem, since a decorator function is expecting just a single function parameter.
But Python recognizes that we’re passing a parameter and changes its behavior slightly. First it calls the decorator function, passing it the parameter. It expects to get back the actual decorator function. Then it invokes that returned decorator function, passing it the wrapped function that triggered all this.
Here’s an example of an implementation:
def Encode (encoding_key): def decor (inner_function): def wrapper (s): s = encode_string(s, encoding_key) return inner_function(s) return wrapper return decor @Encode(key1) def function_1 (params): # ... do function stuff ... return retval #
Let’s take what happens here step by step:
- Python compiles the
- Python sees the
@Encodedecorator takes a parameter, so it invokes Encode passing it
decorfunction, which Python pushes on the decorator stack (expecting to find either more decorators or a function next).
- It’s a function,
function_1. Python compiles the function into an anonymous function object. It does not bind this object to the
- Python pops a decorator (
decor) off the stack and invokes it, passing it the compiled function object.
- The decorator function returns the
wrapperfunction. If there were more decorators in the stack, Python would repeat step #5 for each decorator. When the stack is empty, Python binds the final returned wrapper to the
The addition here is the pre-call to the decorator function to pass it a customizing parameter.
So far, I’ve said the wrapper function has to take the same parameters as the wrapped function. We actually have a little more flexibility in Python.
For one example, we can do something like this:
def MyDecorator (inner_function): def wrapper (*args, **kwargs): # ... modify params ... return inner_function(*args, **kwargs) return wrapper #
For simplicity we’ll assume the wrapper doesn’t care about the return value in this case. (If it needs to, it can follow the pattern shown above.)
If the functions we decorate all take a
**kwargs parameter, this provides almost complete flexibility in mixing and matching parameters:
@MyDecorator def Alfa (a, b, c, **kwargs): # ... do function stuff ... @MyDecorator def Baker (x, y, **kwargs): # ... do function stuff ... @MyDecorator def Charlie (s, **kwargs): # ... do function stuff ... Alfa(21, 42, -18) Baker(y=+0.0013437, x=-0.75, debug=True) Charlie(pre='foo', s='Hello, World!', post='bar') #
As you see there is a great deal of flexibility. When calling functions, arguments can be positional or can have keywords (in which case they can be out of order). Keyword arguments unknown to the function are ignored.
This also works well when
**kwargs isn’t used at all. The
PrintArgs decorator function below can be applied to any function (or class):
def PrintArgs (inner_function): def wrapper (*args): name = inner_function.__qualname__ print('function: %s' % name) for a in enumerate(args): print('>>> [%d]: %s' % a) return inner_function(*args) return wrapper @PrintArgs def Alfa (a, b, c): # ... do function stuff ... @PrintArgs def Baker (x, y): # ... do function stuff ... @PrintArgs def Charlie (s): # ... do function stuff ... Alfa(21, 42, -18) Baker(-0.75, +0.0013437) Charlie('Hello, World!') #
The wrapper just prints the arguments to the function (or to the class being called). It could be used as a debugging tool. If parameterized (as shown in the previous post), a single global variable could turn the printing on or off.
So far this just scratches the surface. A decorator function can return any callable object with any properties and internal methods it needs. You can decorate any function or class. This includes functions inside classes (methods).
Speaking of which, now we turn to decorators for classes and methods.
The latter is nearly identical to what we’ve done so far, the only difference is accounting for the self parameter. If the wrapper uses the
*args pattern I just showed you, that automatically includes the self parameter.
Otherwise, we write the wrapper function ever so slightly differently:
def PrintMethodArgs (method): def wrapper (self, *args): name = method.__qualname__ print('%s: %s' % (name, str(args))) return method(self, *args) return wrapper class Example (object): @PrintMethodArgs def method1 (self, a, b, c): # ... do method stuff ... @PrintMethodArgs def method2 (self, x, y): # ... do method stuff ... @PrintMethodArgs def method3 (self, s): # ... do method stuff ... #
We explicitly include the
self parameter as the first argument to the method.
This eliminates a lurking gotcha involving decorating the
__str__ method of a class. The problem is the
PrintArgs decorator from above tries to print all the arguments using
str(argument). The first of which is the self argument, and
str(self) loops right back to the
__str__ method, ends up in the decorator again, which prints the arguments,… Infinite loop.
PrintMethodArgs decorator doesn’t print the first argument, thus avoiding the loop. (Not entirely reasonable to so decorate the
__str__ method in the first place; it takes no arguments.)
Variations involving the
self parameter aside, method decorators are essentially the same as function decorators. At least until you get fancy with decorators, but that’s a future post.
Decorators for classes follow the same general protocol. The decorator function takes a single parameter, a class object in this case, and returns a wrapper function that will wrap the class.
When Python sees a decorated class definition, it stacks the decorator, compiles the class into an anonymous class object, pops the decorator off the stack and passes the class object as an argument. The binds the wrapper object it receives from the decorator to the class name.
Now references to that class name return the wrapper object. Creating a class instance involves calling the class, so, likewise, calling the wrapper object should return an instance object.
Note that decorator functions can do all sorts of crazy things that alter what they wrap in weird ways. With powerful language structures like this, always remember that can is not should. “With great power comes etc.”
Caveat in mind, the simplest possible class decorator looks like this:
def ClassDecorator (cls): def wrapper (*args): return cls(*args) return wrapper @ClassDecorator class Example (object): # ... class definition ... #
The decorator function takes a class object, which the wrapper function uses to create class instances. The wrapper function takes whatever arguments the class constructor takes — i.e. the arguments passed when creating a new instance.
So a decorator like this can control how an instance is created. Or just log each instance created for debugging purposes.
Parameterizing class decorators works the same as with function decorators. It adds that extra level of first calling the decorator with the parameter argument to get the actual decorator and then calling that with the newly compiled object to be decorated as an argument.
When I pick this up again (possibly not immediately), I’ll explore some of the more sophisticated things you can do with decorators.
It mostly boils down to the ability to return a callable object that can be as powerful as you like.