Unlike the previous examples, this decorator does not alter the function call in an obvious way. No cases exist where you apply this decorator and get a different result from the decorated function than you did from the undecorated function. The previous examples raised exceptions or modified the result if this or that check did not pass. This decorator is more invisible. It does some under-the-hood work, but in no situation should it change the actual result.
Variable Arguments
It is worth noting that the @json_output
and @logged
decorators both provide inner
functions that simply take, and pass on with minimal investigation, variable arguments and keyword arguments.
This is an important pattern. One way that it is particularly important is that many decorators may be used to decorate plain functions as well as methods of classes. Remember that in Python, methods declared in classes receive an additional positional argument, conventionally known as self
. This does not change when decorators are in use. (This is why the requires_user
decorator shown earlier does not work on bound methods within classes.)
For example, if @json_result
is used to decorate a method of a class, the inner
function is called and it receives the instance of the class as the first argument. In fact, this is fine. In this case, that argument is simply args[0]
, and it is passed to the decorated method unmolested.
One thing that has been consistent about all the decorators enumerated thus far is that the decorators themselves appear not to take any arguments. As discussed, there is an implied argument – the method that is being decorated.
However, sometimes it is useful to have the decorator itself take some information that it needs to decorate the method appropriately. The difference between an argument passed to the decorator and an argument passed to the function at call time is precisely that. An argument to the decorator is processed once, when the function is declared and decorated. By contrast, arguments to the function are processed when that function is called.
You have already seen an example of an argument sent to a decorator with the repeated use of @functools.wraps
. It takes an argument – the method being wrapped, whose help and docstring and the like should be preserved.
However, decorators have implied call signatures. They take one positional argument – the method being decorated. So, how does this work?
The answer is that it is complicated. Recall the basic decorators that have execution-time wrapping of code. They declare an inner method in local scope that they then return. This is the callable returned by the decorator. It is what is assigned to the function name. Decorators that take arguments add one more wrapping layer to this dance. This is because the decorator that takes the argument is not actually the decorator. Rather, it is a function that returns the decorator, which is a function that takes one argument (the decorated method), which then decorates the function and returns a callable.
That sounds confusing. Consider the following example where a @json_output
decorator is augmented to ask about indentation and key sorting:
So, what has happened here, and why does this work?
This is a function, json_output
, which accepts two arguments (indent
and sort_keys
). It returns another function, called actual_decorator
, which is (as its name suggests) intended to be used as a decorator. That is a classic decorator – a callable that accepts a single callable (decorated
) as an argument and returns a callable (inner
).
Note that the inner
function has changed slightly to accommodate the indent
and sort_keys
arguments. These arguments mirror similar arguments accepted by json.dumps
, so the call to json.dumps
accepts the values provided to indent
and sort_keys
in the decorator's signature and provides them to json.dumps
in the antepenultimate line.
The inner
function is what ultimately makes use of the indent
and sort_keys
arguments. This is fine, because Python's block scoping rules allow for this. It also is not a problem that this might be called with different values for inner
and sort_keys
, because inner
is a local function (a different copy is returned each time the decorator is used).
Applying the json_output
function looks like this:
And if you run the do_nothing
function now, you get a JSON block back with indentation and newlines added, as shown here:
How Does This Work?
But wait. If json_output
is not a decorator, but a function that returns a decorator, why does it look like it is being applied as a decorator? What is the Python interpreter doing here that makes this work?
More explanation is in order. The key here is in the order of operations. Specifically, the function call (json_output(indent=4)
) precedes the decorator application syntax (@
). Thus, the result of the function call is used to apply the decorator.
The first thing that is happening is that the interpreter is seeing the function call for json_output
and resolving that call (note that the boldface does not include the @
):
All the json_output
function does is define another function, actual_decorator
, and return it. As the result of that function, it is then provided to @
, as shown here:
Now, actual_decorator
is being run. It declares another local function, inner
, and returns it. As previously discussed, that function is then assigned to the name do_nothing
, the name of the decorated method. When do_nothing
is called, the inner
function is called, runs the decorated method, and JSON dumps the result with the appropriate indentation.
The Call Signature Matters
It is critical to realize that when you introduced your new, altered json_output
function, you actually introduced a backward-incompatible change.
Why? Because now there is this extra function call that is expected. If you want the old json_output
behavior, and do not need values for any of the arguments available, you still must call the method.
In other words, you must do the following:
Note the parentheses. They matter, because they indicate that the function is being called (even with no arguments), and then the result is applied to the @
.
The previous code is not —repeat, not —equivalent to the following:
This presents two problems. It is inherently confusing, because if you are accustomed to seeing decorators applied without a signature, a requirement to supply an empty signature is counterintuitive. Secondly, if the old decorator already exists in your application, you must go back and edit all of its existing calls. You should avoid backward-incompatible changes if possible.
In a perfect world, this decorator would work for three different types of applications:
● @json_output
● @json_output()
● @json_output(indent=4)
As it turns out, this is possible,