Skip to main content

Command Palette

Search for a command to run...

Decorators In Python

Published
•4 min read•View as Markdown

What are Decorators?

Decorators are a way to modify or extend the behavior of functions or methods in Python. They allow you to add functionality to existing functions or methods without changing their source code. This is achieved by wrapping the original function or method with another function that provides the desired functionality.

How Do Decorators Work?

In Python, decorators are implemented using the @ symbol followed by the name of the decorator function. This syntax is placed above the function or method that you want to decorate. When the function or method is called, the decorator function is executed first, and then the original function or method is called.

Here's a simple example to illustrate how decorators work:

# Define a decorator function
def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

# Define a function and apply the decorator
@my_decorator
def say_hello():
    print("Hello!")

# Call the decorated function
say_hello()

In this example, the my_decorator function takes another function func as an argument and returns a new function called wrapper. The wrapper function is then used to wrap the original say_hello function. When say_hello is called, the wrapper function is executed first, and then the original say_hello function is called.

Decorators as Tolls on a Road

To better understand decorators, let's consider a real-world analogy. Imagine you are driving on a highway, and you come across a toll booth. The toll booth is like a decorator that modifies your journey on the road. It adds an extra step (paying the toll) before you can continue on your way.

In our example, the my_decorator function is like the toll booth. It takes the original function say_hello as input and returns a new function wrapper. This new function acts as an intermediary step before the original function is executed.

Just like how you can drive on the highway without going through the toll booth, you can call the original function say_hello without applying the decorator. However, if you want to add some extra functionality (like printing messages before and after the function call), you can use the decorator to modify the behavior of the function.

Problem 1: Timing Function Execution

Problem: Write a decorator that measures the time a function takes to execute.

In this problem, we want to measure the time a function takes to execute. We've defined a decorator timer that takes a function func as an argument. This decorator wraps the original function with a new function wrapper that measures the time before and after calling the original function. It then prints the time taken for the function to execute.

import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} ran in {end - start}")
        return result

    return wrapper

@timer
def example_function(n):
    time.sleep(n)

example_function(2)
# Output(after 2 sec): example_function ran in 2.000772476196289

Problem 2: Debugging Function Calls

Problem: Create a decorator to print the function name and the values of its arguments every time the function is called.

In this problem, we want to create a decorator debug_function_call that prints the function name and the values of its arguments every time the function is called. This decorator takes a function func as an argument and wraps it with a new function wrapper that prints the function name and its arguments before calling the original function.

def debug_function_call(func):
    def wrapper(*args, **kwargs):
        args_value = ", ".join(str(arg) for arg in args)
        kwargs_value = ", ".join(f"{k}:{v}"for k, v in kwargs.items())
        print(f"calling: {func.__name__} with args: {args_value} and kwargs: {kwargs_value}")
        return func(*args, **kwargs)
    return wrapper

@debug_function_call
def test_function(name, age, phone):
    return f"My name is {name}, {age} years old, and my phone is {phone}"

test_function("Hruthik", 21, phone=555888333)
# Ouput: test_function with args: Hruthik, 21 and kwargs: phone:555888333

Problem 3: Cache Return Values

Problem: Implement a decorator that caches the return values of a function, so that when it's called with the same arguments, the cached value is returned instead of re-executing the function.

In this problem, we want to implement a decorator cache that caches the return values of a function. This decorator takes a function func as an argument and wraps it with a new function wrapper that caches the return values of the original function using a dictionary cached_args.

import time

def cache(func):
    cached_agrs = {}
    print(cached_agrs)
    def wrapper(*args):
        if args in cached_agrs:
            return cached_agrs[args]
        result = func(*args)
        cached_agrs[args] = result
        return result
    return wrapper

@cache
def long_running_function(a, b):
    time.sleep(4)
    return a + b

print(long_running_function(2, 3))
print(long_running_function(2, 3))

# Output:
# {}
# 5 (prints after 4 sec)
# 5 (prints immediately as it was cached)

See you in the next article where we will be building a Project.