Understanding Python Decorators
We use decorators in many places. When we create an API route, dataclass & etc. But how it works few people know. Want to cover through this article what are they and how we can build our own.
If you use Flask or FastAPI or any framework to build API you use a decorator to create a route. If you are thinking of caching then the decorator is there or schedule a task for that also decorator is there like that the list goes on.
But what exactly is Decorator?
In simple words, you can say it's just a function.
If you want me to explain in more words then we can say A Python decorator is a function that takes in a function and returns it by adding some functionality.
Decorator is an amazing feature of many programming languages which allows us to modify or extend the behaviour of functions or methods without changing their actual code.
If you want to do something before and after a function then Decorators are the best option to explore.
In the normal world, you can say decorators are like Front gate security at the office. What they care about is who is coming to the office, what they are taking to the office & what they are taking from the office after their work is done. They don’t care what you are doing in the office.
Before understanding Decorator let’s understand Python functions a bit in depth so that we can understand Decorators a lot better.
Let’s Understand Python Functions
We all know what a function is: it takes arguments as input, does some work using it then returns a value.
Functions are like First class citizens
In functional programming, you work mostly with pure functions which don’t have side effects. By the way, Python supports many functional programming concepts, including treating functions as first-class objects. It means a function can be used as an input argument and also we can return a function.
Function as an input argument
def entry_log(name):
return f"{name} entering into the office"
def exit_log(name):
return f"{name} exiting from the office"
def security_check(name, log_func):
print(log_func(name))
Here entry_log & exit_log are normal functions which write regularly but the security_check function is not like that; it expects function as 2nd input arguments. So we can pass entry_log or exit_log to it.
security_check("Ashok", entry_log)
security_check("Ashok", exit_log)
Console Output:
Ashok entering into the office
Ashok exiting from the office
In these kinds of cases just remember to send the function name that’s it, brackets are not required if we add brackets means we are asking it to execute instead of passing it.
Function as a Return Value
Like we can send functions as input arguments we can return also. Let’s see the same example with a bit of change to see that in action.
def entry_log(name):
return f"{name} entering into the office"
def exit_log(name):
return f"{name} exiting from the office"
def get_security_check_function(is_entering):
if is_entering:
return entry_log
else:
return exit_log
security_check = get_security_check_function(True)
print(security_check)
print(security_check("Ashok"))
security_check = get_security_check_function(False)
print(security_check("Ashok"))
Console Output:
<function entry_log at 0x713ce62b3d90>
Ashok entering into the office
Ashok exiting from the office
Here also if you want to return a function then don’t add brackets to it. It basically returns that function; we can just execute that by adding brackets & input arguments if required.
You can see the console log basically returns the reference to the function.
Inner Functions (Function inside a Function)
By the way, we can write functions under functions which are called inner functions. The inner functions scope will be up to the parent function level. It means we can’t call them from the outside. But the parent function wants then it can send its reference that we can call.
Let me show you an example:
def parent():
print("Printing from parent()")
def child_function():
print("Printing from child_function()")
return child_function
child = parent()
print("Checking what's in the child: ", child)
child()
child_function()
Console Output:
Printing from parent()
Checking what's in the child: <function parent.<locals>.child_function at 0x73680808a0e0>
Printing from child_function()
Traceback (most recent call last):
File "/path/to/folder/check.py", line 61, in <module>
child_function()
NameError: name 'child_function' is not defined
We called the parent function so its print statement runs & then it just returns the child function.
If you see what got returned you can see the child_function reference but it clearly shows it’s under parent.
We can run it using brackets.
But if we want to call the child_function directly it will say it’s not defined because the child_function scope is up to the parent function only.
I hope you got a fair understanding of functions as First Class citizens/objects.
Now let’s understand Decorators
Simple Decorator Example
Let’s see a simple example & understand
def decorator(func):
def wrapper():
print("Before Function")
func()
print("After Function")
return wrapper
def hello_world():
print("Hello World!")
hello_world_function = decorator(hello_world)
hello_world_function()
We just created a decorator function which takes the function as an argument and we are just wrapping whatever we want to do before and after the function call which came as an argument and returning that wrapper.
When we call the decorator function by giving the hello_world function as an argument we are getting the internal function wrapper so we are calling that function. We will get the output like this.
Console Output:
Before Function
Hello World!
After Function
If you understood this part then you kind of understood the decorator part totally.
Let’s add Syntactic Sugar to it
Now let's see the magic. Instead of calling the decorator function by passing another function as an argument, let's add syntactic sugar to it. Now the code looks like this.
def decorator(func):
def wrapper():
print("Before Function")
func()
print("After Function")
return wrapper
@decorator
def hello_world():
print("Hello World!")
hello_world()
What we did was add a decorator to the hello_world function. If you run this code same output comes.
See writing decorator is simple and the internal functionality also very simple.
Of Course, it is a simple version. We need to add a couple of whistles & bells to it like if the passed function takes input arguments or returns something which also we should take care of.
The Problem with this Approach
This way we can play with decorators but this is not the right way because of this we will lose the metadata of the original function.
Let me show you an example
def decorator(func):
def wrapper():
print("Before Function")
func()
print("After Function")
return wrapper
@decorator
def hello_world():
'''
This is a function to print Hello World!
'''
print("Hello World!")
help(hello_world)
If we write this code and execute the program we will get output like this
Console output:
Help on function wrapper in module __main__:
wrapper()
See the whole essence of the hello_world function gone. hello_world function doesn’t have a wrapper because of the decorator this wrapper function is coming. If you remove the decorator syntactic sugar part which is on top of the hello_world and run the help function it shows proper metadata. Like this.
Console output:
Help on function hello_world in module __main__:
hello_world()
This is a function to print Hello World!
The solution to it lies with @functools.wraps
Now let’s write the decorator part properly.
import functools
def decorator(func):
@functools.wraps(func)
def wrapper():
print("Before Function")
func()
print("After Function")
return wrapper
@decorator
def hello_world():
'''
This is a function to print Hello World!
'''
print("Hello World!")
hello_world()
help(hello_world)
Same stuff just added the decorator. If you run this code you can see the proper output & proper metadata of hello_world.
In this example, we are not taking care of the input arguments & return part. So a good example code for a decorator is
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Before Function tasks
return_value = func(*args, **kwargs)
# After Function tasks
return return_value
return wrapper
Some Real World Examples
Timing Decorator
There are several places we can use. Usually, whenever we see some part of an application running slow we calculate the time at a bunch of places in which one is giving trouble so let’s write a decorator for it.
import functools
import time
def calculate_time(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
value = func(*args, **kwargs)
end_time = time.time()
run_time = end_time - start_time
print(f"{func.__name__}() took {run_time} secs")
return value
return wrapper
@calculate_time
def sum_of_numbers(limit):
sum = 0
for number in range(1, limit + 1):
sum += number
print(f"Sum of numbers from 1 to {limit} is {sum}")
sum_of_numbers(999999)
If you run this code you will get output like this.
Console Output:
Sum of numbers from 1 to 999999 is 499999500000
sum_of_numbers() took 0.05221295356750488 secs
Cool right. We made a cool decorator which calculates the time that function took.
We can pass arguments to the decorator also. We can write class-level decorators also like @dataclass. Like that, there are so many things we can do with decorators.
Example for passing arguments to the decorator
import functools
import time
def slow_the_function(seconds=1):
def decorator_code(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
time.sleep(seconds)
value = func(*args, **kwargs)
print(f"Slowed the {func.__name__}() for {seconds} seconds")
return value
return wrapper
return decorator_code
@slow_the_function(seconds=2)
def sum_of_numbers(limit):
sum = 0
for number in range(1, limit + 1):
sum += number
print(f"Sum of numbers from 1 to {limit} is {sum}")
sum_of_numbers(999999)
In this case, the only problem is we need to write a bunch of nested functions other than that cool.
Reference:
https://www.programiz.com/python-programming/decorator
https://realpython.com/primer-on-python-decorators/
https://www.geeksforgeeks.org/decorators-in-python/