Decorators in Python

Aug. 31, 2009 · 11 min read

Logo APSL

What is a decorator?

A decorator is the name of a design pattern. Decorators dynamically alter the functionality of a function, method, or class without having to subclass or change the source code of the decorated class. In the Python sense, a decorator is something else, it includes the design pattern, but it goes further, Bruce Eckel assimilates them to Lisp macros. Decorators and their use in our programs help us to make our code cleaner, to self-document it and, unlike other languages, they do not require us to learn another different programming language (as happens with Java annotations for example). In their use we can simulate aspect-oriented programming (AOP) or use them to add control systems to our functions, log, cache... The possibilities are endless. Decorators have been part of Python since version 2.4 and, as Michele Simionato says, they provide us with the following: They reduce common and repetitive code (the so-called boilerplate code). They favor the separation of code responsibilities. They increase readability and maintainability. Decorators are explicit.

Decorators Classification

We can divide decorators into groups:

  • According to the parameters they admit:
    • They do not admit parameters.
    • They do admit parameters.
  • Depending on whether they preserve the signature or signature of the method they decorate:
    • Decorators that do not preserve the signature.
    • Decorators that do preserve it.

The simplest decorators are those that they take no parameters and do not preserve the signature.

A do-nothing decorator

To begin with, we will create a decorator that will convert any function into /dev/null, that is, it will not return anything and will not do anything with the function. We will call our decorator forat_negre, black hole.

def forat_negre(f): def none(): pass return none @forat_negre def di_hola(): return "hola"

If we execute di_hola() we will not have any result, rather, we will have None as the result of the function we are decorating.

The @ syntax of the Python decorator is what is called syntactic sugar, that is, a way of writing things that increases the readability of the code. However, we must keep in mind that the decorator could have been written as: di_hola = forat_negre(di_hola) di_hola() and we would have the same decorator.

Let's remember that functions are first-class objects in PYthon and that they can be assigned and passed as parameters. Although the example is very simple, it helps us to see the following: A decorator is nothing more than a function wrapper and therefore has to return a function, more specifically a callable, that is, anything other than putting a double parenthesis around the side () doesn't generate an error.

def retorna_objecte(f): ....: def obj(): ....: return object() ....: return obj ....: In [17]: def di_hola(): ....: return "Hola" ....: In [18]: di_hola = retorna_objecte(di_hola) In [19]: di_hola() Out[19]:

We have passed a function without parameters to our decorator forat_negre. If we try to pass it a function with parameters we will find a little surprise…

@forat_negre def suma(a,b): return a,b suma(2,3) TypeError Traceback (most recent call last) TypeError: none() takes no arguments (2 given)

which on the other hand is completely normal, we have defined the black_hole in such a way that it returns a function without parameters, so if we try to pass the parameters that the decorated function had, it simply complains and asks. We are going to define our decorator a little better so that this does not happen to us and that it can admit at least as many parameters as the function that decorates.

def forat_negre(f): "d'aquí no surt res" def none(*args, **kw_args): pass return none @forat_negre def suma(a,b): "suma dos parametres qualsevols si pot" return a+b suma(2,2)

Now it no longer gives an error. So we have another conclusion: in addition to returning a function, we have to ensure that the definition of the function that we return admits at least the same number of parameters as the function that we want to decorate. If we don't know how many there are, we heal with args and kw_args. Notice that we have not maintained the signature of the function. If as an experiment you try to do a help(suma) we don't get the help of the original function. We will come back to this a bit later. So for now we know how to create simple decorators from a function.

Making decorators non-intrusive

If you have done a help(suma) or a suma._name_ perhaps some of you will have been surprised to see that the name of the function is _none_ instead of the expected suma. If you review what we have done, it is not surprising either: we have replaced the original function with another. We recall that the decorator f applied to the function g is equivalent to setting g = f(g). You would want the decorator to be able to maintain the documentation and name of the function it decorates, as this would make the function easier to use and code autocompletors wouldn't go so crazy. We can do this in two ways: the long and the short.

The long way

def forat_negre(f): def none(*args, **kw_args): pass none.__doc__= f.__doc__ none.__dict__= f.__dict__ none.__name__= f.__name__ return none

With the three additional instructions that we have put, we return to retrieve the metadata of the original function that we passed to the decorator. If we now do a help we will see that it returns the name of the correct function suma and that the help is also yours.

Help on function suma in module __main__: suma(*args, **kw_args) Suma dos parametres qualsevols si pot

The short way

Preserving metadata is quite useful and common, in fact in the functools module we find the wraps function which is itself a decorator and does just that. In this way the previous code would be:

from functools import wraps def forat_negre(f): @wraps(f) def none(*args, **kw_args): pass return none

Notice that we have used a decorator to create another decorator. We'll see more use of decorators a bit later.

A decorator with arguments

The decorator that we programmed in the previous section was quite simple, it did very little and had no parameters. If we want to create decorators we have to make them useful first of all, and we will also find the need for these decorators to admit parameters. In Django, for example, you can find that the cache decorator admits parameters that allow us to tell it how long it has to cache the results, or the vary_on_headers decorator, which allows us to modify the content of the views response by adding the headers that we indicate... Let's see how we can achieve it. There are also two ways to do it, the clear and the complex.

The clear way is the one we recommend and uses a class to make the decorator, the complex one requires more effort to understand what the decorator is doing, it is shorter, but I prefer more readable code. The decorators that we have programmed as functions can also be created as classes, but in this case, I think that the definition in the form of functions is easier to follow, and it will allow us to clearly distinguish between the two types of decorators: those that do not allow parameters which are preferably built using functions and those that admit parameters, which are preferably built using classes. To continue with the black hole, now in our example, we will show the result of the function that we decorate or not randomly. For this, we will pass to the decorator a function as a parameter that, when executed, will determine if the result of the decorated function has to be displayed or not.

The clear method of making decorators with arguments

#!/usr/bin/env python # -*- coding: UTF-8 -*- import random class forat_negre_sonat(object): "Un decorador amb fam" def __init__(self, mostrar): self.mostrar = mostrar def __call__(self, f): def none(*args, **kw_args): if self.mostrar(): return f(*args, **kw_args) else: return "Nop" return none @forat_negre_sonat(mostrar = lambda :random.choice((True, False))) def suma(a, b): "Suma dos elements que li passam com a paràmetre" return a+b if __name__=="__main__": print suma(2,3) print suma(5,6) print suma(9,5)

Let's look at the code:

  1. We have created a Python class whose constructor (__init__) can take the parameter or parameters we want. It's a normal constructor, so it takes parameters by default.
  2. Remember that we have said that the decorator has to be a callable object, in a class, the callability is given by the method call. We will define this class in such a way that it obtains the function to decorate with a parameter. In this way, we have access both to the decorator parameters, which we have passed to the constructor and to the decorated function, which we have passed as a parameter to the call.

After this, all that remains is to encapsulate the call as we did in the previous case, returning the decorator instead of the function to decorate. In the example, I have also tried to show that the parameter can be whatever we want, specifically I have passed an anonymous function, created with lambda, which is responsible for establishing the randomness of the result. If you want, we can make this decorator a bit more complete, making it admit values in addition to functions and preserving the name and documentation of the decorated function.

#!/usr/bin/env python # -*- coding: UTF-8 -*- import random class forat_negre_sonat(object): "Un decorador amb fam" def __init__(self, mostrar=None): self.mostrar = mostrar def __call__(self, f): def none(*args, **kw_args): if callable(self.mostrar): opcion = self.mostrar() else: opcion = self.mostrar if opcion: return f(*args, **kw_args) else: return "Nop" none.__name__ = f.__name__ none.__doc__ = f.__doc__ return none @forat_negre_sonat(mostrar = lambda :random.choice((True, False))) def suma(a, b): "Suma dos elements que li passam com a paràmetre" return a+b @forat_negre_sonat(mostrar=True) def resta(a,b): return a-b if __name__=="__main__": print "Exemple amb %s " % suma.__name__ print suma(2,3) print suma(5,6) print suma(9,5) print "Exemple amb %s " % resta.__name__ print resta(2,3) print resta(5,6)

The complex method of creating decorators with parameters

def forat_negre_dos(mostrar): def wrap(f): @wraps(f) def wrapped_function(*args, **kw_args): if callable(mostrar): opcion = mostrar() else: opcion = mostrar if opcion: return f(*args, **kw_args) else: return "Nop" return wrapped_function return wrap

Well, convoluted, what is said to be convoluted is not, since such a simple thing does not have much history, but notice that the code that follows is much worse. The first thing we have done is define our function, where we have put the parameters it admits. This function returns another function that admits one argument, which is the function to decorate, which in turn admits an indeterminate number of arguments (remember that we are forcing this). Since the second function, wrapped_function is defined inside wrap, it has access to the decorator parameter and can use it.

Chaining decorators

Decorators can be chained, that is, a function can have as many decorators as necessary and we need, only limited by our common sense and the readability of the program. Two decorators are common, three are not seen much, and four or more are to think about it. For the example, I will borrow from the Python Decorator Library one of the most useful decorators, el memoize, which allows us to cache a function from its parameters. The Python Decorator Library's implementation of the memoize pattern is fairly easy to follow with what we now know, and it will also help us complete the construction of parameterless decorators using a class.

class memoized(object): """Decorator that caches a function's return value each time it is called. If called later with the same arguments, the cached value is returned, and not re-evaluated. """ def __init__(self, func): self.func = func self.cache = {} def __call__(self, *args): try: return self.cache[args] except KeyError: self.cache[args] = value = self.func(*args) return value except TypeError: # uncachable -- for instance, passing a list as an argument. # Better to not cache than to blow up entirely. return self.func(*args) def __repr__(self): """Return the function's docstring.""" return self.func.__doc__

Unlike the construction with parameters, in the constructor of the memoized class, the function to be decorated is put as a parameter, and the function's parameters are placed in the call method, instead of the function to be decorated as was done to the other method. Why has this way been used if the other is simpler? Well, why do we need to keep the cache in memory and what is done is to keep it in a dictionary within the same class? If the cache was external (with memcached for example), it could have been done perfectly as a function. We will also define a decorator that will serve to indicate when we enter the function and check the memoized decorator.

def log(f): "Registra l'execució de la funció" def wrap(*args): print "Excutant %s, args: %s" % \\ (f.__name__, ",".join(str(x) for x in args)) return f(*args) return wrap @memoized @log def fibonacci(n): "Return the nth fibonacci number." if n in (0, 1): return n return fibonacci(n-1) + fibonacci(n-2) print fibonacci(12)

Try running this code with and without the memoized function. With both decorators active, you will see that each decorator takes as input the already decorated function that comes out of the decorator below it. Thus the memoized takes as input the fibonacci function already decorated with the log. You can do the test with a simpler example:

#!/usr/bin/env python # -*- coding: UTF-8 -*- def uppercase(f): "Dada una función f que devuelve un string lo pasa todo a mayúsculas" def wrap(): return f().upper() return wrap def make_bold(f): "Dada una función f que devuelve un string le añade los tags de bold" def wrap(): return "%s" % f() return wrap @make_bold @uppercase def say_hello(): return "Hello world" print say_hello()

Try changing the order of the decorators and you will see perfectly how they are applied from the function upwards. In the example, the "Hello word" is first converted to upper case and then the bold tags are applied to it.

The pending signature

Before finishing we have one pending issue: the signature. The decorators we've created can preserve the name and documentation of the function they decorate, but they don't preserve the signature, that is, the number of parameters we pass to it. Michele Simionato has written an excellent module called decorator that extends the use of decorators, keeping the function signature, name and documentation, and also gives us the possibility to create decorator factories. A tool to always have on hand. With this module we could write the code of the previous example as:

from decorator import decorator @decorator def uppercase(f, *args): "Donada una funció f que retorna un string ho passa a majúscules" return f(*args).upper() @decorator def make_bold(f, *args): "Afegeix el tag strong a la sortida de la funció" return "%s" % f(*args) @uppercase @make_bold def say_hello(nom): "Di hola, home!" return "Hello world %s" % nom if __name__=="__main__": from inspect import getargspec print say_hello('World') print say_hello.func_name print say_hello.__doc__ print getargspec(say_hello)

If you run the code you will see that we did not need to resort to wraps or reassign the name, the Simionato library itself has done it. Also, if we look at the output of the example:

HELLO WORLD WORLD say_hello Di hola, home! ArgSpec(args=['nom'], varargs=None, keywords=None, defaults=None)

The first line corresponds to the output of the function that we have decorated. The second is the name of that function. We see the name of the original function and not the name of the decorator. The documentation has also been maintained and finally, we can see that the signature of the function is correct, it tells us that it has a mandatory argument called nom.

Conclusion

I hope I have made the subject of decorators a little clearer. Creating them is not difficult, using them is even simpler, we just have to be clear about what they are and when to use them. They are a powerful tool that allows us to make our code more readable and cohesive. Enjoy the decorators without fear! Like everything in this life, use them with common sense and moderation.

References

To write this article I have relied on different sources, the most interesting I quote below:

  • PEP 318 Decorators I : Introduction to Python Decorators
  • Decorators II: Decorator Arguments
  • Python Decorators
  • Understanding decorators
  • Charming Python: Decorators make magic easy
  • Decorator 3.1.2 Decorator Pattern Python decorator Library
Comparte este artículo
Recent posts