The Python programming language has lots of amazing concepts and techniques that make writing code easy for programmers. Decorators are one of them.
A decorator is a function that takes another function as an argument and adds more functionality to the function without modifying its components.
In this post, I'll be explaining how decorators work and situations where they can be useful.
In order to know how decorators work, we need to understand two fundamental Python concepts; First-class functions and Closures.
First-Class Functions
In programming, any language entity that can operate like other language entities is referred to as a "first-class object". By extension, if a programming language treats functions as first-class objects, then it is said to support first-class functions and any function written in that particular language can be referred to as a first-class function.
The concept of having functions as first-class objects is supported in Python, and because functions are first-class citizens in Python, we can;
- Store them in variables.
- Pass them as arguments to other functions
- Return them from other functions.
- Store them in data structures.
Being able to treat functions like other objects and data types like strings and Integers is one of the major reasons why decorators are possible.
An example that displays first-class attributes of functions in Python
OUTPUT
6 multiplied by 2 is equal to 12
Now that we understand a little about what first-class functions are, lets move on to Closures.
Closures
Just like we have nested loops in Python, we also have nested functions. Nested functions are functions that are defined inside other functions, and a closure is just a nested function that has access to the variables of its enclosing function even if they are not present in memory.
An example of closure
OUTPUT
6 multiplied by 2 is equal to 12
By making use of a closure in the above example, we can see that the outer_func function could still be invoked even from outside its scope.
Decorators
Now that we have a basic understanding of First-class functions and Closures, we are one step closer towards understanding Python decorators and how they work.
Just as mentioned at the beginning of this article, decorators can modify the behaviour of another function without messing with the source code of the function.
Lets go ahead and create a simple example of a decorator.
Example of a simple decorator
OUTPUT
This decorator adds more functionality to the sum_func function
3 + 4 = 7
We can see from this example how two of the concepts we've previously explained are used.
The sum_decorator function modifies the sum_func function and then returns the wrapper function.
This is just one way of writing decorators. Another method of writing decorators in Python is by using the @ symbol. Using this syntax, all we need to do to decorate a function is just the @ symbol. This syntax is more popular and you're more likely to come across decorators being written this way.
Here is an example:
Example of a simple decorator using the @ syntax
OUTPUT
This decorator adds more functionality to the sum_func function
3 + 4 = 7
We can see that we still get the same output as our previous example, and this is so because using the @ symbol before the sum_decorator function and putting it above sum_func automatically passes sum_func as an argument of sum_decorator without us needing to write it the way we've been doing in the previous examples.
Accepting arguments in Decorators
There are certain cases where it might be necessary for our decorators to accept arguments. In cases like that, what we do is pass the arguments to the wrapper function, and those arguments will be subsequently passed down to the decorated function when it is called.
example of a decorated function with arguments
OUTPUT
This decorator adds more functionality to the sum_func function
3 + 4 = 7
Again, our output remains the same, but this time, the value to the variables being used are not predefined like earlier.
We were able to achieve this using *args and **kwargs.
**Kwargs and *args are two special syntaxes in Python that can be passed to functions so as to enable them accept series of keyworded and non-keyworded arguments.
Using them in this context gives our decorator the ability to decorate any function regardless of the number of arguments it may require.
It should be noted that using the terms "kwargs" and "args" is not exactly compulsory,they're just conventions that have been widely adopted by the community.
Chaining Multiple Decorators
It is possible to apply more than one decorator to a single function. When the code gets executed, the decorators will be applied in the order they were called.
OUTPUT
Another decorator to extend wrapper
This decorator adds more functionality to the sum_func function
3 + 4 = 7
From this example, it is evident that decorators are applied to a function in an ascending order.
Going back, we'll notice that there was an error in the output for this example. Our required output was supposed to be;
OUTPUT
Another decorator to extend sum_func
This decorator adds more functionality to the sum_func function
3 + 4 = 7
But we got this instead;
OUTPUT
Another function to extend wrapper
This decorator adds more functionality to the sum_func function
3 + 4 = 7
Obviously, another_decorator wasn't created to extend the wrapper function, but our output says otherwise. This anomaly occurs because when a function is wrapped with a decorator, the wrapper closure hides the metadata of the function and replaces it with its own. So anytime we want to access the metadata of a decorated function, we get that of the wrapper instead.
This seemingly simple problem can cause lots of complications in certain cases (just like the above example).
In the case of our example, what happened was that we used sum_decorator as our argument for another_decorator instead of using sum_func, and that is why func.name returns "wrapper" instead of "sum_func".
To fix this error, we make use of an inbuilt Python decorator; wraps.
The wraps decorator can be found in the functools module. The wraps decorator helps us solve this issue by copying the metadata from the function (before being decorated) to the decorated closure.
OUTPUT
Another decorator to extend sum_func
This decorator adds more functionality to the sum_func function
3 + 4 = 7
By making use of wraps, we're able to get our desired output.
Application of Decorators
Apart from the fact that decorators help reduce code repetition, here are some other cases where a decorator might come in handy;
- Authorization
- Logging
- Memoizing
- Synchronization
Further reading:
In case you're interested in learning more about Python decorators, here are some other resources I can recommend for your learning;
If you enjoyed reading this, here is the link to another post of mine where I explained the concept of Big O Notation: UNDERSTANDING BIG O NOTATION