I’ve been playing around with what Python calls decorators. They’re a built-in way of implementing Aspect-Oriented Programming techniques in Python. In fact, they’re quite powerful.
Since they aren’t a common language feature, they can be a little confusing at first, so I thought I’d try my hand at laying out how they work.
First, Aspect-Oriented Programming (AOP) is a way of writing code that becomes a prequel, sequel, or wrapper, for other functions.
One canonical example involves a suite of banking routines the must log and validate the caller before proceeding. Since every routine does this, AOP allows calling a single validation prequel routine as part of any call into the suite.
A caveat is that the AOP routines need to know about the suite routines. In particular, they need to know the inputs and outputs. Further, a single AOP routine must be able to handle all the routines it wraps, which often means those routines must all take the same parameters.
So there can be limits to AOP, but, it depends on the language. Reflective languages such as Python are less limited because the wrapper function can find out about the wrapped function at run-time.
§
Let’s suppose we have a suite of functions that take string commands. For definiteness, let’s say the suite controls a telescope. For brevity, our example will have just two functions (but imagine there are more):
- scope_azimuth: Controls the azimuth (left-right) setting.
- scope_elevation: Controls the elevation (up-down) setting.
Both functions (and the others we’re imagining) take a single command string. That string contains keywords and numbers separated by spaces. The functions expect proper input: no leading or trailing spaces, no extra spaces, and the keywords must be in UPPERCASE.
A command might need to look like: +168.3721 RATE 3
That might set the telescope azimuth to +168.3721 at a rate of 3 (some predetermined setting for motor speed). Commands to other functions would look similar.
[It’s actually criminal the functions are that limited, but this is just a toy example to illustrate decorators and AOP. In reality, the kind of processing we’re doing here would be easily built-in to the functions.]
Our problem is that the command will come from user input, so it may not be as clean as it should be. It may contain those extra, leading or trailing spaces; the may be in uppercase, lowercase, or any combination.
So we have a situation where each function has the same signature (each takes a single string) and each function needs to do the same processing to that string. Leading and trailing spaces must be stripped, extra spaces removed, and text forced to the expected case.
§
A perfectly valid approach, one available in just about any language, looks like this:
002| # process the string
003| return s
004|
005| def scope_azimuth (command):
006| command = process_command(command)
007| # …azimuth code…
008|
009| def scope_elevation (command):
010| command = process_command(command)
011| # …elevation code…
012|
Each function just calls process_command()
first and uses the processed return value.
§
Python decorators let us do it another way. Let’s start very simply and take it step by step:
002| def wrapper (s):
003| s = s.upper()
004| return inner(s)
005| return wrapper
006|
007| @UpperCase
008| def scope_azimuth (command):
009| # …azimuth code…
010|
011| @UpperCase
012| def scope_elevation (command):
013| # …elevation code…
014|
The first function, UpperCase
, is our decorator function. It expects to receive a function, called inner
. I’ll come back to that. When called, UpperCase
returns a function, the wrapper
function, which is defined inside it.
The wrapper
function “wraps” the inner function. The wrapper is called when the code calls the telescope functions. Therefore it has to also take the expected single string parameter.
Now the inner
parameter passed to the UpperCase
function is relevant. The parameter is the function to be wrapped (hence the name “inner”). In this example, it will be each telescope function in turn. The wrapper
function calls the inner
function. (One could use a decorator to replace a function, say temporarily for debugging, in which case one wouldn’t call the wrapped function.)
In this case, wrapper
first forces the input string to uppercase, then invokes the wrapped function, passing it the uppercase string. The wrapper just returns whatever the wrapped function returns.
§
One key here is that the decorator function returns a function. That returned function is the wrapper, so it must have a compatible signature with what it wraps.
There are other options, which I’ll explore in future posts. (For example, the decorator function can return an object if that object is callable. It can even return a non-callable object, but the code will blow when it tries to treat it as a function.)
It’s important to understand that Python changes the binding of the original function. In the example above, scope_azimuth
and scope_elevation
are bound to separate anonymous instances of the outer
function. The scope functions originally defined under those names are not bound to them and can only be called from inside the decorator.
The equivalent is something like this:
002| return lambda s:inner(s.upper())
003|
004| def scope_azimuth (command):
005| # …azimuth code…
006| def scope_elevation (command):
007| # …elevation code…
008|
009| scope_azimuth = UpperCase(scope_azimuth)
010| scope_elevation = UpperCase(scope_elevation)
011|
Except that there is no momentary binding of the scope_azimuth
and scope_elevation
names to the functions.
(The Uppercase()
routine is functionally the same as above, but here I’ve used a shorter more Pythonish definition.)
§
Let’s take it up a notch. What if there is a need for more than one wrapper?
Suppose we decide we like the idea of a UpperCase
decorator we can use for any function that takes a string. In fact, we’d like to add a matching LowerCase
decorator to go with it and add them to our general toolkit.
We haven’t tackled the problem of spaces in the command string. We could, and normally probably would, create a decorator just for the telescope functions — a decorator that both forces case and cleans up spaces — but in this case we’re creating simple one-purpose tools we can use generally.
So how about a decorator that strips leading and trailing spaces and reduces multiple spaces to a single space. As a bonus, it can convert other whitespace, such as tabs or newlines, to spaces.
Our three decorators might look like this:
002| return lambda s:inner(s.upper())
003|
004| def LowerCase (inner):
005| return lambda s:inner(s.lower())
006|
007| def Spaces (inner):
008| def wrapper (s):
009| t = s.split()
010| return inner(‘ ‘.join(t))
011| return wrapper
012|
The LowerCase
and Uppercase
functions use the shorter way of writing the wrapper. The Spaces
function is simple enough it could also, but I used the longer form for clarity.
When we want multiple decorators, we apply them in sequence as shown below:
002| @Spaces
003| def scope_azimuth (command):
004| # …azimuth code…
005|
006| @UpperCase
007| @Spaces
008| def scope_elevation (command):
009| # …elevation code…
010|
In both cases the function names bind to the UpperCase
function, which calls the Spaces
function, which calls the inner wrapped function.
What happens is that, upon encountering the source code, Python recognizes the decorators and pushes them on a stack until it hits the function definition. It compiles the function to create an anonymous function object. Then it pops the last decorator off the stack and invokes it, passing the function object as a parameter.
Python assumes the invoked decorator returns a function (the wrapper function), and it treats this as it did the original function. It pops the next decorator off the stack and invokes it, passing it the returned wrapper function object.
Again, Python assumes it gets back a (wrapper) function object. If there were more decorators objects on the stack, the process would repeat until the stack is empty. Ultimately the compiler ends up with the last wrapper function.
This it binds to the original function name. Calling that function now passes the argument through the wrappers until it reaches the original function body.
The equivalent (comparable to the earlier equivalent shown above) is something like this (but, again, there is no temporary binding of the scope_*
names):
002| # …azimuth code…
003| def scope_elevation (command):
004| # …elevation code…
005|
006| scope_azimuth = UpperCase(Spaces(scope_azimuth))
007| scope_elevation = UpperCase(Spaces(scope_elevation))
008|
The decorator chain can be as long as desired, although I can’t think of many applications that would use more than one. The main use for decorators normally is that they implement the @staticmethod
and @classmethod
functions.
I so far have not thought of a serious production use for them, but they are a neat tool.
§
That’s all for this time.
So far we’ve been processing the inputs to the inner function. We can also process the outputs. (Obviously the wrapper function needs to know what the inner function returns.)
I’ll start there when I pick up the decorator topic again. There is also the matter of decorators that take parameters — those work slightly differently.
I’ll also explain how decorators can be used inside a class. Down the line I’ll explain how we can get really fancy with decorators.
Ø
There is a definite connection with currying in how decorator functions take a single parameter, a compiled function object (or class object), and return a function.
(Neither the wrapper function nor the wrapped function use currying, though.)