Python decorator pattern example

Einblick Content Team - February 13th, 2023

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

Note on @wraps from functools

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.

Start using Einblick

Pull all your data sources together, and build actionable insights on a single unified platform.

  • All connectors
  • Unlimited teammates
  • All operators