Decorators in Python
Contents
Why do we use functions?
Functions were introduced in programming to reduce clutter and to improve reusability and readability. Any piece of code which
- repeated itself multiple time
- had an independent context
could be taken away and recreated as a function. This function was then called wherever that piece of code was required.
As the code grows, we may have some code inside a function which is repeated, and has a meaning independent of the parent function. We can again take that piece of code outside the function, and create another function out of it.
And Decorators?
Decorators follow somewhat same coding pattern. They are constructs that allow us to change the way function behaves at runtime, and add new features to it which are otherwise independent of that function.
Let’s take a function which formats and prints the contents of a logfile. For some cases, you also want to add security to it so that only the valid users can execute it. What do you do? You go ahead and change the body of the function, adding security inside of it.
Then after some days, you need to add the same security logic to the login module of your application. While we’re at it, let’s add the same security logic to the file manager of the app. Does it make sense to open up the existing code and add some reusable functionality which is independent of the module logic? This is where decorators shine.
Decorators allow us to enhance or entirely change the logic of a function at runtime, without changing the body of a function.
We use functions in order to avoid repetitive code, i.e. any piece of code which repeats itself multiple times is hidden behind a function. Using the same logic if we have some code that is a part of multiple functions, and is independent of the function’s core behavior, can be hidden in a decorator. During runtime, the decorator modifies the behavior of the function, without modifying function’s body.
Useful Concepts
Before jumping into the code of decorators, let’s check out what is already present for us.
Functions as Objects
Python provides the functionality of treating functions as first class objects, so anything that can be done with objects can be done with functions.
We can
- Pass them around
|
|
- Store them in tuples/lists
|
|
- Add attributes
|
|
Inner functions and closures
Functions can also contain other functions and these inner functions have access to the data of the parent function, even after the outer function is no longer present in the scope.
|
|
Syntax
Decorators are just functions which take a function, modify and return
it. Returning a function is an important aspect of decorating the
function. Whatever a decorator returns has to be callable, it can either
be another function, or a class which implements __call__
method.
The syntax is straight forward:
|
|
And we can use it like this:
|
|
And behind the scenes, this happens:
|
|
So the original function is replace with the modified function.
Some examples which make sense
Let’s just say that you have some very expensive functions which are finding solutions to some of the greatest problems ever encountered by mankind. These aren’t called just like that since they’re expensive computationally, and hence we’ll have to log each one of them.
One way of doing that is to add the logging statements inside each of the functions, something like:
|
|
Now adding something like this has multiple bad things. * We are going against the practice of DRY (Don’t repeat yourself) * The logging isn’t required by the function to do its job – it’s an addon. So it shouldn’t be part of it.
So we sense that it’s repeated code, and repeated code needs to have a function for itself. But function inside function?? Yes that’s possible and we can have something like:
|
|
But again, we are including something inside a function that shouldn’t be part of it. It’s code inside a function that repeats itself, but isn’t really needed inside it. So in situations like these, we can start using generators.
|
|
What happened? We just created a decorator. Like I mentioned before, a decorator is a function which changes another function, in a way enhances it’s working. So this generator takes in an expensive function and returns another function which
- print the log time
- calls expensive function.
And how’s it different from the previous approach? Because it’s nicer to look, for starters. This is how we use it:
|
|
While this doesn’t seem much at such small scale, the most important point in the favor of decorators is that they are independent of function body. So, let’s say you have to do some extra logging in one of the functions; with the previous approach, we’ll have to change the name of the function call inside the expensive function, like:
|
|
With generators, we don’t have to touch the body of the expensive function:
|
|
Still not impressed? Let’s take the thing a bit further:
Decorator chaining
Decorator syntax is influenced by mathematical functions. In
mathematics, the statement: h(g(f(x)))
means that function g will take
the the output of f, and function h will take use to the output of g for
the calculation.
You can chain decorators as well:
|
|
This modifies the function f like this:
|
|
Passing arguments in decorators
Since decorator is a function, we can pass arguments to it as well. These arguments can then go and change the way a decorator behaves and hence change the way it decorates the function.
Uptil now, we were using decorators without any arguments, and the syntax was:
|
|
And the decorator body was returning a callable, either a function or a class which implemented call method. I’ll repeat the code here once again to be more clear:
|
|
The syntax of decorator with arguments is:
|
|
And since the decorator is being called here (it’s clear from the () operator), the decorator call itself should return a callable. It’s another way of saying that since decorator is a function, this syntax means that the function call should return another function. How can we achieve that? Like this:
|
|
Since both the argument list of darg* and the decoratee function func are accessible inside wrapped\_func, we can use them to modify the function in whatever ways we desire.
Let’s take the case wherein we want the logging decorator to be able to can print the date and time based on some specific format. We will pass the format as argument of the decorator.
|
|
With this, the major ideas with respect to decorators are covered. The possibilities of using decorators to create reusable code, and removing the clutter from functions are there.
Author Tushar Tyagi
LastMod Jan 21, 2015