Python Decorators
Decorators are a significant part of Python. In simple words: they are functions which take other functions as inputs and output their modified versions.
Sources:
- Python Tips: Decorators
First Class Functions
Recalling that in Python, functions are first class objects, which means:
- A function is an instance of the Object type.
- You can store the function in a variable.
- You can pass the function as a parameter to another function.
- You can return the function from a function.
- You can store them in data structures such as hash tables, lists, …
1 | def hi(name="yasoob"): |
Defining functions within functions
So those are the basics when it comes to functions. Let's take your knowledge one step further. In Python we can define functions inside other functions:
1 | def hi(name="yasoob"): |
So now we know that we can define functions in other functions. In other words: we can make nested functions. Now you need to learn one more thing, that functions can return functions too.
Returning functions from within functions
It is not necessary to execute a function within another function, we can return it as an output as well:
1 | def hi(name="yasoob"): |
Just take a look at the code again. In the if/else
clause we are turning greet
and welcome
, not greet()
and welcome()
.
Why is that? It's because when you put a pair of parentheses after it, the function gets executed; whereas if you don't put parenthesis after it, then it can be passed around and can be assigned to other variables without executing it. Did you get it? Let me explain it in a little bit more detail. When we write a = hi()
, hi()
gets executed and because the name is yasoob by default, the function greet
is returned. If we change the statement to a = hi(name = "ali")
then the welcome
function will be returned. We can also do print hi()()
which outputs now you are in the greet() function.
Giving a function as an argument to another function
1 | def hi(): |
Now you have all the required knowledge to learn what decorators really are. Decorators let you execute code before and after a function.
Decorators
In the last example we actually made a decorator! Let's modify the previous decorator and make a little bit more usable program:
1 | # A decorator function |
Now the original function a_function_requiring_decoration
is wrapped by the decorator a_new_decorator
, it returns a new function a_decorated_function
.
@wrap
syntax
The decorator feature is supported by Python directly with @wrap
syntax.
1 | # A decorator function |
It's just the same as the previous syntax:
1 | def a_function_requiring_decoration(): |
To conclude, the @wrap
syntax is as:
1 | def a_new_decorator(func): |
Change the decorated function name
Since the decorator just substitudes the original function with a wrapper fucuntion, it's easy to see that the wrapper function and the original function are not the same object.
1 | print(a_function_requiring_decoration.__name__) |
However, most of the time we want to make the decorator be "transparent" and let the name of the wrapper function be the same as that of original function.
Luckily, Python provides us a simple function to solve this problem and that is functools.wraps
. Let's modify our previous example to use functools.wraps
:
1 | from functools import wraps |
Now that is much better. Let's move on and learn some use-cases of decorators.
Nesting a Decorator Within a Function
@wrap
is a function itself. So it can be functional programmed as well. For instace, we can call it and let it return a function. In other worlds, we can nest a decorator within a function
1 | def Outer_func(param): |
Note: function Outer_func
is not a decorator itself. Instead, it returns a decorator a_new_decorator
. Thus, the wrapper in @Outer_func('some param')
of syntax @wrapper
is Outer_func('some param')
, not Outer_func
.
Example: add_to_class(Class)
This decorator can register objects as methods in created class.
1 | def add_to_class(Class): |
Look how it works. Given class A
and function do
, we want to register do
as a member function of class A
. Note, as a member function, do
must set it's first parameter as self
.
1 | class A: |
There're 2 ways to do it:
First, we can use the plain syntax:
1 | def do(self): |
Or, we can either use @
syntax:
1 |
|
Example: Decorators with Arguments
Come to think of it, isn't @wraps
also a decorator? But, it takes an argument like any normal function can do. So, why can't we do that too?
This is because when you use the @my_decorator
syntax, you are applying a wrapper function with a single function as a parameter.
Remember, everything in Python is an object, and this includes functions! With that in mind, we can write a function that returns a wrapper function.
Let's go back to our logging example, and create a wrapper which lets us specify a logfile to output to.
1 | def logit(logfile='out.log'): |
We can use this decorator logit
in 2 ways.
First, use the plain syntax:
1 | def myfunc1(): |
This code is quite compact, it actually goes through 3 steps:
1 | func_logging_decorator_with_log_file = logit('out.log') |
The 2nd way is to use @
syntax:
1 | # == logging_decorator |
Constrains of FP
However, the functional programming has it's contrains. It's hard to debug compared to OO programming. Consider following example:
1 | def outer_func(param): |
Here, we first create a decorator function object a_new_decorator
and fix it's inner variable param
as "some params"
.
But the IDE can't see/visit this value when debugging.