Simplifying Decorators in Python | A step-by-step approach.
Last Updated on January 29, 2024 by Editorial Team
Author(s): Akhil Theerthala
Originally published on Towards AI.
Simplifying Decorators in Python U+007C A step-by-step approach.
Python is one of the simpler languages that we can learn these days. However, despite being the simplest one, it still has a few areas that need focused attention to be correctly understood. One such topic is the use of decorators. When I first started learning about it, it was not directly clear on what this meant. However, after repeated encounters with the decorator, I finally took some time to understand the mechanism of a decorator.
Imagine this scenario. You are working on a project where you have to add all the numbers up to a real number k as fast as possible. Now, there are multiple methods to do this. You can keep adding all the numbers through a for
loop. You can directly use the expression for the sum of first n natural numbers. Or you can try to come up with something else.
def add_directly(k):
result = 0
for i in range(k):
result+=i+1
return result
def use_expression(k):
return k*(k+1)/2
Now both of these functions get our job done. However, if you recall, we need a method that is fastest. Now, in order to achieve this, we need to find the time each of these functions takes. You can directly run these functions two times and print the time each of the functions takes.
start = time.time()
print(add_directly(30000))
end = time.time()
print(fβTime taken by adding directly: {end-start} \nβ)
start = time.time()
print(use_expression(30000))
end = time.time()
print(fβTime taken by the expression: {end-start} \nβ)
If you noticed, this violates the common rule DRY (Donβt repeat yourself). So, what can we do? We can write a new function whose purpose is to evaluate the time taken by a different function.
def evaluate_time(func):
"""
Evaluates the time taken for a function to run.
Args:
func (function): any generic function
"""
k = 300000
start = time.time()
print(fβFunction Output: {func(k)}
end = time.time()
return end-start
Now, when you want the time taken by each function, you just use the evaluate_time
function that we previously defined. We are now close to the final meaning of a decorator, but not quite there yet.
Now that we are clear with the intent behind wrapping a function, we need to tidy things up. If we want to run the wrapped function, then we need to run it like this, evaluate_time(add_directly)
. When we want to test the wrap for tens or sometimes hundreds of functions, we need to keep repeating this wrapper(func)
every time. This is once again contradicting the DRY rule. So, what do we do?
Once again, we define a function to wrap a different function. This function is what we call a decorator.
def time_decorator_function(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f'Time taken {func.__name__}: {end-start}')
return result
return wrapper
@time_decorator_function
def add_directly(k):
result = 0
for i in range(k):
result+=i+1
return result
@time_decorator_function
def use_expression(k):
return k*(k+1)/2
What happened here?
Well, we have basically wrapped the functions with the additional lines of code, so that every time we run the add_directly
function, we print the time it took to run. i.e., we changed the add_directly
to a wrapper
which contains the previous version of the add_directly
. This is very similar to how we set x=x+1
, i.e. just updating the value stored in x.
This is what the outputs look like now,
With the basic understanding, we can now add all the personal knowledge, like defining attributes for the wrapper and other things to play around with when needed. People tend to use decorators for multiple purposes like caching, logging, unit tests, authentication/authorization (in Flask and Django), etc.
It is now in your hands to play around and decorate whatever functions you find useful. However, I just want to add another cherry to what we have discussed so far. Remember what I said about decorators being similar to x=x+1
? Well, they truly are similar, and this process loses all the metadata (like doc_strings) we add to the function.
See it for yourself by running the code below:
def time_decorator_function(func):
def wrapper(*args, **kwargs):
"""
This just wraps over the input func
"""
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(fβTime taken {func.__name__}: {end-start}β)
return result
return wrapper
@time_decorator_function
def add_directly(k):
"""
Adds the numbers between 1 and k directly.
"""
result = 0
for i in range(k):
result+=i+1
return result
@time_decorator_function
def use_expression(k):
return k*(k+1)/2
print(add_directly.__name__)
print(add_directly.__doc__)
To retain the metadata, we often use the functools.wraps
decorator on our wrapper
function, which adds the original metadata to the wrapper
function. For example, see the code block below,
import functools
def custom_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
pass
return wrapper
Now, as homework, try adding the decorator by yourself and see the attributes yourself.
β –
With this understanding as the foundation, you can now play around with the different forms of decorators and keep writing wonderful code.
With this understanding as the foundation, you can now play around with the different forms of decorators and keep writing wonderful code. Letβs meet again with another simple concept that we often get confused with and discuss it. If you like such articles, subscribe to Neuronuts to get notified as soon as a new one is released. You can also follow Neuronuts on Instagram and LinkedIn to stay connected.
Thanks for reading! Letβs meet next time!U+270CοΈ
Originally published at https://www.neuronuts.in.
Join thousands of data leaders on the AI newsletter. Join over 80,000 subscribers and keep up to date with the latest developments in AI. From research to projects and ideas. If you are building an AI startup, an AI-related product, or a service, we invite you to consider becoming aΒ sponsor.
Published via Towards AI