Decorators are a powerful and flexible feature of Python that allow you to modify the behavior of a function or method without modifying the base function’s underlying code. This can be useful for applying common patterns or behaviors to multiple functions or methods, without repeating the same code over and over again. In this post, we’ll go over basic syntax and an example that evaluates code performance.
Fork the canvas below to test out the code yourself. Now we'll show you how you can write your own Python decorator:
Python decorator basic syntax
from functools import wraps
# Define a decorator function, which takes another function as an argument
def my_decorator(my_func):
@wraps(my_func) # @wraps preserves function metadata like name and docstring
def wrapper(*args, **kwargs):
# do something before my_func is called
print("Doing something first.")
result = my_func(*args, **kwargs)
# do something after my_func is called
print("Doing something after.")
return result
return wrapper
@my_decorator # invoke decorator
def my_func(x, y):
"""This function returns the sum of x and y"""
print("my_func is running.")
return x + y
my_func(5, 3)
Output:
Doing something first.
my_func is running.
Doing something after.
Out[35]: 8
We defined a decorator function, my_decorator
, it takes a function as an argument and returns a new function, called wrapper
, which adds additional behavior before and after the original function is called.
We then use the @my_decorator
syntax to apply the my_decorator
decorator to the my_func
function. This means every time my_func
is called, the behavior that my_decorator
invokes will occur before and after the behavior defined by my_func
occurs.
Overall, decorators are a useful and powerful tool to have in your Python toolkit for data science tasks. They can help you apply common patterns and behaviors to multiple functions or methods in a concise and elegant way.
Time decorator example
Here’s an example of using a decorator to time a function’s performance:
# Define a time_decorator function that times a given function
def time_decorator(my_func):
@wraps(my_func)
def wrapper(*args, **kwargs):
start = perf_counter()
result = my_func(*args, **kwargs)
end = perf_counter()
print(f"{my_func.__name__}({args[0]}) | Time elapsed: {str(end - start)}")
return result
return wrapper
# Naive factorial calculation
@time_decorator
def fact_n(n):
"""This is a recursive, computationally expensive way to calculate a factorial"""
if n < 2:
return 1
else:
return n * fact_n(n-1)
fact_n(15)
Output:
fact_n(1) | Time elapsed: 4.700850695371628e-07
fact_n(2) | Time elapsed: 1.585087738931179e-05
fact_n(3) | Time elapsed: 2.0211096853017807e-05
fact_n(4) | Time elapsed: 2.4210894480347633e-05
fact_n(5) | Time elapsed: 2.870103344321251e-05
fact_n(6) | Time elapsed: 3.23709100484848e-05
fact_n(7) | Time elapsed: 3.569107502698898e-05
fact_n(8) | Time elapsed: 3.877095878124237e-05
fact_n(9) | Time elapsed: 4.187086597084999e-05
fact_n(10) | Time elapsed: 4.485086537897587e-05
fact_n(11) | Time elapsed: 4.8310961574316025e-05
fact_n(12) | Time elapsed: 5.161087028682232e-05
fact_n(13) | Time elapsed: 5.4921023547649384e-05
fact_n(14) | Time elapsed: 5.8341072872281075e-05
fact_n(15) | Time elapsed: 6.284099072217941e-05
Out[33]: 1307674368000
@wraps
from functools
Note on You'll notice from the earlier code that before defining the wrapper
function in each decorator definition, there is a line: @wraps(my_func)
, essentially a decorator within the decorator function.
from functools import wraps
def my_decorator(my_func):
@wraps(my_func) # @wraps preserves function metadata like name and docstring
def wrapper(*args, **kwargs):
# Some Python code
return
return
@my_decorator # invoke decorator
def my_func(x, y):
"""This function returns the sum of x and y"""
print("my_func is running.")
return x + y
wraps
comes from the functools
module, and allows the preservation of the name and docstring of my_func()
:
print(my_func.__name__)
print(my_func.__doc__)
Output with @wraps
:
my_func
This function returns the sum of x and y
Without the @wraps
decorator, the __name__
and __doc__
attributes would be taken from the wrapper
function within the decorator function:
print(my_func.__name__)
print(my_func.__doc__)
Output without @wraps
:
wrapper
None
About
Einblick is an AI-native data science platform that provides data teams with an agile workflow to swiftly explore data, build predictive models, and deploy data apps. Founded in 2020, Einblick was developed based on six years of research at MIT and Brown University. Einblick is funded by Amplify Partners, Flybridge, Samsung Next, Dell Technologies Capital, and Intel Capital. For more information, please visit www.einblick.ai and follow us on LinkedIn and Twitter.