Creating Custom Decorators in Python.

Gurpreet Singh
4 min readSep 28, 2024

--

Python decorators are used to modify the function’s behavior without changing their source code. You can add functionality by wrapping a function with a decorator and dynamically enhancing its functionality, promoting code reuse, and maintaining a clean and modular codebase. In this advanced tutorial, we will cover the process of creating custom decorators and demonstrate how they would be applied in real-world projects.

Understanding Decorators

Before diving into the creation process, let’s quickly review the fundamentals of decorators:

  • A decorator is a higher-order function that takes a function as input, adds some functionality, and returns a new function.
  • Decorators operate on functions, allowing you to modify their behavior without directly altering their source code.
  • Decorators follow the principle of single responsibility, ensuring each decorator focuses on a specific task or enhancement.
  • Python provides a convenient syntax using the @ symbol to apply decorators to functions.

Creating a Simple Decorator

Let’s start by creating a simple decorator that measures the execution time of a function:

from time import time

def measure_time(func):
def wrapper(*args, **kwargs):
start_time = time()
result = func(*args, **kwargs)
end_time = time()
print(f"Function {func.__name__} took {end_time - start_time:.6f} seconds to execute.")
return result
return wrapper

@measure_time
def fibonacci(n):
if n <= 1:
return n
else:
return fibonacci(n-1) + fibonacci(n-2)

fibonacci(35)

In this example:

  1. The measure_time decorator takes a function func as input.
  2. Inside the decorator, a nested function wrapper is defined, which accepts any number of arguments using *args and **kwargs.
  3. The wrapper function measures the start time, calls the original function func with the provided arguments, measures the end time, and prints the execution time.
  4. The wrapper function returns the result of the original function call.
  5. The measure_time decorator returns the wrapper function.
  6. The @measure_time decorator is applied to the fibonacci function, modifying its behavior without changing its source code.

Decorators with Arguments

Sometimes, you may want to pass arguments to your decorator. This can be achieved by creating a decorator factory, which is a function that returns a decorator:

def repeat(n):
def decorator(func):
def wrapper(*args, **kwargs):
result = None
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator

@repeat(3)
def greet(name):
print(f"Hello, {name}!")

greet("Alice")

In this example:

  1. The repeat decorator factory takes an argument n, which specifies the number of times to repeat the decorated function.
  2. Inside repeat, a decorator function is defined, which takes the original function func as input.
  3. The nested wrapper function is defined within the decorator, which calls the original function n times and returns the last result.
  4. The decorator function returns the wrapper function.
  5. The repeat decorator factory returns the decorator function.
  6. The @repeat(3) decorator is applied to the greet function, causing it to be called three times with each invocation.

Decorators with Classes

You can also create decorators using classes. This approach allows you to maintain state and provides more flexibility in the decoration process:

class CacheDecorator:
def __init__(self, func):
self.func = func
self.cache = {}

def __call__(self, *args, **kwargs):
key = str(args) + str(kwargs)
if key in self.cache:
return self.cache[key]
else:
result = self.func(*args, **kwargs)
self.cache[key] = result
return result

@CacheDecorator
def expensive_function(a, b):
# Perform some expensive computation
return a ** b

result1 = expensive_function(2, 10)
result2 = expensive_function(2, 10)

In this example:

  1. The CacheDecorator class takes the original function func in its __init__ method and stores it as an instance variable.
  2. The class also maintains a cache dictionary to store the results of previous function calls.
  3. The __call__ method is defined, which is invoked when an instance of the class is called like a function.
  4. Inside __call__, a unique key is generated based on the function arguments.
  5. If the key exists in the cache, the cached result is returned; otherwise, the original function is called, the result is cached, and the result is returned.
  6. The @CacheDecorator decorator is applied to the expensive_function, which now caches its results based on the input arguments.

Common Mistakes to Avoid

When creating custom decorators, be aware of the following common mistakes:

  1. Forgetting to return the wrapper function: Make sure to return the wrapper function from the decorator, or the original function will not be properly decorated.
  2. Modifying the signature of the decorated function: If the decorated function expects specific arguments, ensure that the wrapper function has a compatible signature.
  3. Forgetting to handle *args and **kwargs: If the decorated function can accept any number of positional or keyword arguments, make sure to handle them in the wrapper function using *args and **kwargs.
  4. Applying decorators to methods in classes: When decorating methods, be cautious about the self argument and ensure that it is properly handled in the wrapper function.
  5. Overusing decorators: While decorators are powerful, use them judiciously. Excessive use of decorators can make the code harder to understand and maintain.

Practical Applications of Decorators

Decorators have a wide range of applications in Python programming. Here are a few examples:

  • Caching: Decorators can be used to cache the results of expensive function calls, improving performance.
  • Authentication and Authorization: Decorators can enforce access control by verifying user authentication or authorization before executing a function.
  • Logging and Debugging: Decorators can be used for consistent logging of function calls or handling errors and exceptions across multiple functions.
  • Rate Limiting: Decorators can be employed to limit the number of function calls within a specific time frame, preventing abuse or overloading of resources.
  • Validation: Decorators can validate input arguments or return values, ensuring data integrity and consistency.

By understanding the fundamentals of decorators and their creation process, you can design and implement custom decorators tailored to your specific needs, leading to more modular, maintainable, and extensible Python code.

--

--

Gurpreet Singh
Gurpreet Singh

Written by Gurpreet Singh

🧠 Turning ideas into code and impact. From Django to React and AWS—crafting magic with AI and innovation. Always chasing the next challenge. 🚀✨

No responses yet