Professional Python. Luke Sneeringer
a decorator that modifies its behavior based on the arguments that it receives. Remember, a decorator is just a function and has all the flexibility of any other function to do what it needs to do to respond to the inputs it gets.
Consider this more flexible iteration of json_output
:
This function is endeavoring to be intelligent about whether or not it is currently being used as a decorator.
First, it makes sure it is not being called in an unexpected way. You never expect to receive both a method to be decorated and the keyword arguments, because a decorator is always called with the decorated method as the only argument.
Second, it defines the actual_decorator
function, which (as its name suggests) is the actual decorator to be either returned or applied. It defines the inner
function that is the ultimate function to be returned from the decorator.
Finally, it returns the appropriate result based on how it was called:
● If decorated_
is set, it was called as a plain decorator, without a method signature, and its responsibility is to apply the ultimate decorator and return the inner
function. Here again, observe how decorators that take arguments are actually working. First, actual_decorator(decorated_)
is called and resolved, then its result (which must be a callable, because this is a decorator) is called with inner
provided as its only argument.
● If decorated_
is not set, then this was called with keyword arguments instead, and the function must return an actual decorator, which receives the decorated method and returns inner
. Therefore, the function returns actual_decorator
outright. This is then applied by the Python interpreter as the actual decorator (which ultimately returns inner
).
Why is this technique valuable? It enables you to maintain your decorator's functionality as previously used. This means that you do not have to update each case where the decorator has been applied. But you still get the additional flexibility of being able to add arguments in the cases where you need them.
Decorating Classes
Remember that a decorator is, fundamentally, a callable that accepts a callable and returns a callable. This means that decorators can be used to decorate classes as well as functions (classes are callable, after all).
Decorating classes can have a variety of uses. They can be particularly valuable because, like function decorators, class decorators can interact with the attributes of the decorated class. A class decorator can add or augment attributes, or it can alter the API of a class to provide a distinction between how a class is declared versus how its instances are used.
You might ask, "Isn't the appropriate way to add or augment attributes of a class through subclassing?" Usually, the answer is "yes." However, in some situations an alternative approach may be appropriate. Consider, for example, a generally applicable feature that may apply to many classes in your application that live in distinct places in your class hierarchies.
By way of example, consider a feature of a class such that each instance knows when it was instantiated, and instances are sorted by their creation times. This has general applicability across many different classes, and requires the addition of three attributes – the instantiation timestamp, and the __gt__
and __lt__
methods.
You have multiple ways to go about adding this. Here is how you can do it with a class decorator:
The first thing that is happening in this decorator is that you are saving a copy of the class's original __init__
method. You do not need to worry about whether the class has one. Because object
has an __init__
method, that attribute's presence is guaranteed. Next, you create a new method that will be assigned to __init__
, and this method first calls the original and then does one piece of extra work, saving the instantiation timestamp to self._created
.
It is worth noting that this is a very similar pattern to the execution-time wrapping code from previous examples – making a function that wraps another function, whose primary responsibility is to run the wrapped function, but also adds a small piece of other functionality.
It is worth noting that if a class decorated with @sortable_by_creation_time
defined its own __lt__
and __gt__
methods, then this decorator would override them.
The _created
value by itself does little good if the class does not recognize that it is to be used for sorting. Therefore, the decorator also adds __lt__
and __gt__
magic methods. These cause the <
and >
operators to return True
or False
based on the result of those methods. This also affects the behavior of sorted
and other similar functions.
This is all that is necessary to make an arbitrary class's instances sortable by their instantiation time. This decorator can be applied to any class, including many classes with unrelated ancestry.
Here is an example of a simple class with instances sortable by when they are created:
Bear in mind that simply because a decorator can be used to solve a problem, that does not mean that it is necessarily the appropriate solution.
For instance, when it comes to this example, the same thing could be accomplished by using a "mixin," or a small class that simply defines the appropriate __init__
, __lt__
, and __gt__
methods. A simple approach using a mixin would look like this:
Applying the mixin to a class can be done using Python's multiple inheritance:
This approach has different advantages and drawbacks. On the one hand, it will not mercilessly plow over __lt__
and __gt__
methods defined by the class or its superclasses (and it may not be obvious when the code is read later that the decorator was clobbering two methods).
On the other hand, it would be very easy to get into a situation where the __init__
method provided by SortableByCreationTime
does not run. If MyClass
or MySuperclass
or any class in MySuperclass
's ancestry defines an __init__
method, it will win out. Reversing the class order does not solve this problem; it simply reverses it.
By contrast, the decorator handles the __init__
case very well, simply by augmenting the effect of the decorated class's __init__
method and otherwise leaving it intact.
So, which approach is the correct approach? It depends.
Type Switching
Thus far, the discussion in this chapter has only considered cases in which a decorator is expected to decorate a function and provide a function, or when a decorator is expected to decorate a class and provide a class.
There is no reason why this relationship must hold, however. The only requirement for a decorator is that it is a callable that accepts a callable and returns the callable. There is no requirement that it return the same kind of callable.
One more advanced use case for decorators is actually when they do not do this. In particular, it can be valuable for a decorator to decorate a function, but return a class. This can be a very useful tool for situations where the amount of boilerplate code grows, or for allowing developers to use a simple function for simple cases, but subclass a class in an application's API for more advanced cases.
An example of this in the wild is a decorator used in a popular task runner in the Python ecosystem: celery. The celery package provides a @celery.task
decorator that is expected to decorate a function. What the decorator actually does is return a subclass of celery's internal Task
class, with the decorated function