Tags
Aspect-Oriented Programming, computer language, programming language, Python 101, Python code, Python decorator
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:
02| def wrapper (params):
03| # … modify params …
04| return inner_function(params)
05| return wrapper
06|
07| @MyDecorator
08| def some_function (params):
09| # … do function stuff …
10| return retval
11|
The decorator function, MyDecorator
, takes a parameter, inner_function
(the function to be wrapped), defines and returns a function, wrapper
.
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:
02| def wrapper (params):
03| # … modify params …
04| retval = inner_function(params)
05| # … modify retval …
06| return retval
07| return wrapper
08|
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:
02| def function_1 (params):
03| # … do function stuff …
04| return retval
05|
06| @Encode(key2)
07| def function_2 (params):
08| # … do function stuff …
09| return retval
10|
This 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:
02| def decor (inner_function):
03| def wrapper (s):
04| s = encode_string(s, encoding_key)
05| return inner_function(s)
06| return wrapper
07| return decor
08|
09| @Encode(key1)
10| def function_1 (params):
11| # … do function stuff …
12| return retval
13|
Let’s take what happens here step by step:
- Python compiles the
Encode
function. - Python sees the
@Encode
decorator takes a parameter, so it invokes Encode passing itkey1
. Encode
returns thedecor
function, 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 thefunction_1
name. - Python pops a decorator (
decor
) off the stack and invokes it, passing it the compiled function object. - The decorator function returns the
wrapper
function. 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 thefunction_1
name.
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:
02| def wrapper (*args, **kwargs):
03| # … modify params …
04| return inner_function(*args, **kwargs)
05| return wrapper
06|
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:
02| def Alfa (a, b, c, **kwargs):
03| # … do function stuff …
04|
05| @MyDecorator
06| def Baker (x, y, **kwargs):
07| # … do function stuff …
08|
09| @MyDecorator
10| def Charlie (s, **kwargs):
11| # … do function stuff …
12|
13| Alfa(21, 42, –18)
14| Baker(y=+0.0013437, x=–0.75, debug=True)
15| Charlie(pre=‘foo’, s=‘Hello, World!’, post=‘bar’)
16|
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):
02| def wrapper (*args):
03| name = inner_function.__qualname__
04| print(‘function: %s’ % name)
05| for a in enumerate(args):
06| print(‘>>> [%d]: %s’ % a)
07| return inner_function(*args)
08| return wrapper
09|
10| @PrintArgs
11| def Alfa (a, b, c):
12| # … do function stuff …
13|
14| @PrintArgs
15| def Baker (x, y):
16| # … do function stuff …
17|
18| @PrintArgs
19| def Charlie (s):
20| # … do function stuff …
21|
22| Alfa(21, 42, –18)
23| Baker(–0.75, +0.0013437)
24| Charlie(‘Hello, World!’)
25|
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:
02| def wrapper (self, *args):
03| name = method.__qualname__
04| print(‘%s: %s’ % (name, str(args)))
05| return method(self, *args)
06| return wrapper
07|
08| class Example (object):
09| @PrintMethodArgs
10| def method1 (self, a, b, c):
11| # … do method stuff …
12| @PrintMethodArgs
13| def method2 (self, x, y):
14| # … do method stuff …
15| @PrintMethodArgs
16| def method3 (self, s):
17| # … do method stuff …
18|
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.
The 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:
02| def wrapper (*args):
03| return cls(*args)
04| return wrapper
05|
06| @ClassDecorator
07| class Example (object):
08| # … class definition …
09|
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.
Ø