Pre-requisite: this awesome stackoverflow answer on decorators.
I had been trying to learn Flask and came across this nice post. It contains a good introduction to flask in the form of a mini project. While going through the post, I came across this snippet:
@app.route('/') is a decorator which registers the index function to be called when a user makes GET requests to the root page.
I always get the part where we say that a decorator is a function which modifies another function - wraps it with a sort of pre and post functionality. The part of where decorators are passed arguments is what used to confuse me and led me to revisit the afforementioned stackoverflow post
For example, it is easy to understand this:
Output:
Decorator calling func
In the function
Decorator called func
which means that when Python encountered the @ symbol it did an internal equivalent of
func = decorator(func)
which in turn means that a decorator is a function which takes in a function and returns a wrapper over that function and reassigns that wrapper to the original function variable.
This has the side effect of redefining the function name also (func.__name__), to be that of the wrapper function, but as the stackoverflow answer mentions functools.wraps comes to the rescue.
What used to stall me were these kind of examples:
Output:
func1 args - [this, that]
func2 called_from_line:31 args - [who, what]
For func1 our logging is not as verobse as func2 which has 'debug' log level. This is possible because the decorator creating function takes an argument which decides logging behavior (whether to print line no. of caller or not).
The magic of closures is also involved because the decorator and the wrapped function remember the original environment that they were bound with (for func1 the variable loglevel is set to 'info' in wrapped_around_func and for func2, it is set to 'debug')
But what is the deal with the nesting of functions?
It's clear to get if one uses the right function names for each function in the example. Here is the modified version of the previous code snippet, with changes only in the function names:
Output:
func1 args - [this, that]
func2 called_from_line:31 args - [who, what]
So we call a log_decorator_factory which defined one or more decorators - (only 1 in this case - log_decorator_1). This decorator is responsible for returning the function which will wrap the decorator targets - func1 and func2 in this case.
This was the moment of clarity for me - either you can choose a decorator depending on the argument passed to the decorator factory or you can use the argument and create different sections of code within one decorator. We did the later in the previous example.
To do the former, check out this piece of code:
Output:
func1 args - [this, that]
func2 called_from_line:39 args - [who, what]
The output and the decorator interface to func1 and func2 being the same, what is changing is that the factory defines 2 decorators: log_decorator_1, log_decorator_2 - and the right one is returned depending on the argument.
This might seem to be overkill for some - defining 2 functions when creating a logical branch in one could suffice. I agree. But I find it useful to remember the concept of decorators this way.
The way I say it in my mind:
A decorator factory defines one or more decorators and depending on the argument passed to the decorator factory - the right decorator's wrapper function is returned.
or
A decorator factory defines a single decorator and depending on the arugment, the decorator performs logical branching (if-else) and adds/removes functionality from the returned decorator wrapper function.
This also goes to prove that if your decorator is not going to take an argument - there isn't a need for an explicit decorator factory - you can directly call the decorator. Case in point being:
The above code could also have been written this way:
yielding the same output. The only difference in the interface being that instead of doing @decorator, we do @decorator_factory() which means - invoke the factory, return a decorator wrapper and use that wrapper to wrap the original function.
But as we are not taking any arguments for the factory to pass them on to the decorator and it's wrapper - what is the point of having a factory? Hence we do away with the decorator_factory level of nesting and just have - decorator and decorator_wrapper as seen earlier.
So that's how I see it. I hope this discussion helped you in refining your thoughts about decorators. Please feel free to share your insights and views on the same in the comment section.
In my next post, I will take up the @app.register('/') chunk of code we encountered earlier and try to explain its internals.