Mastering Decorators in Python: A Comprehensive Guide with Examples

Satish Pophale
5 min readMar 25, 2023

--

Photo by Rubaitul Azad on Unsplash

Decorate your code with Python decorators to enhance its functionality, reusability, and aesthetics

  1. Introduction to decorators
  2. Function decorators
  3. Class decorators
  4. Decorators with __call__ method
  5. Preserving metadata with wraps
  6. Creating argumented decorators
  7. Creating decorator that work with both function and classes
  8. Chaining multiple decorators and their ordering

Introduction to decorators

In Python, decorators are a way to modify or enhance the behavior of a function without changing its source code. They are implemented as functions that take a function as an argument and return a new function that wraps around the original function.

Decorators can be used to add functionality such as logging, timing, caching, authorization, and validation to a function. They can help simplify code by separating cross-cutting concerns from business logic.

def my_decorator(func):
def wrapper():
print("Before function is called.")
func()
print("After function is called.")
return wrapper

@my_decorator
def my_function():
print("Hello, World!")

my_function()

In this example, my_decorator is a function that takes a function as an argument and returns a new function wrapper that adds some additional functionality before and after the original function is called. The @my_decorator syntax is used to apply the decorator to the my_function.

Function decorators

Function decorators are the most common type of decorators in Python. They are used to modify the behavior of a function by wrapping it in another function.

import time

def timing_decorator(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"Function {func.__name__} took {end - start:.2f} seconds to run.")
return result
return wrapper

@timing_decorator
def my_function():
time.sleep(2)

my_function()

In this example, timing_decorator is a function decorator that calculate times how long a function takes to run. The wrapper function calculates the start and end times and prints the duration of the function. The @timing_decorator syntax is used to apply the decorator to the my_function.

Class decorator

Class decorators are similar to function decorator’s but they are applied to classes instead of functions. They are used to modify the behavior of a class by wrapping it in another class.

def my_decorator(cls):
class NewClass(cls):
def new_method(self):
print("New method added.")
return NewClass

@my_decorator
class MyClass:
def original_method(self):
print("Original method called.")

obj = MyClass()
obj.original_method()
obj.new_method()

In this example, my_decorator is a class decorator that adds a new method to a class. The NewClass class is defined with the new method and it inherits from the original class cls. The @my_decorator syntax is used to apply the decorator to the MyClass.

Decorators with __call__ method

Decorators can also be created using classes with the __call__ method. This method allows the class to be called like a function and can be used to create decorators that take arguments.

class my_decorator:
def __init__(self, arg1, arg2):
self.arg1 = arg1
self.arg2 = arg2

def __call__(self, func):
def wrapper(*args, **kwargs):
print(f"Arguments passed to decorator: {self.arg1}, {self.arg2}")
result = func(*args, **kwargs)
return result
return wrapper

@my_decorator("Hello", "World")
def my_function():
print("Hello, World!")

my_function()

In this example, my_decorator is a class that takes two arguments in its constructor and implements the __call__ method to create a decorator. The wrapper function prints the arguments passed to the decorator before calling the original function. The @my_decorator("Hello", "World") syntax is used to apply the decorator to the my_function.

Preserving metadata with wraps

When creating decorators, it’s important to preserve the metadata of the original function such as its name, docstring, and parameters. This can be done using the wraps function from the functools module.

from functools import wraps

def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Before function is called.")
result = func(*args, **kwargs)
print("After function is called.")
return result
return wrapper

@my_decorator
def my_function():
"""This is the docstring of my_function."""
print("Hello, World!")

print(my_function.__name__)
print(my_function.__doc__)

In this example, the @wraps(func) syntax is used to preserve the metadata of the original function func. The my_function.__name__ and my_function.__doc__ statements print the name and docstring of the original function.

Creating argumented decorators

Decorators can also take arguments to modify their behavior. This can be useful for creating generic decorators that can be customized for different use cases.

def repeat(num):
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for i in range(num):
print(f"Function {func.__name__} called {i+1} times.")
result = func(*args, **kwargs)
return result
return wrapper
return my_decorator

@repeat(num=3)
def my_function():
print("Hello, World!")

my_function()

In this example, the repeat function takes an argument num that specifies the number of times the function should be called. The my_decorator function is defined inside the repeat function and it uses the wrapper function to execute the original function num times.

Creating decorators that work with both functions and classes

While most decorators are designed to work with functions, you can also create decorators that work with classes. This can be useful if you want to modify the behavior of a class or its methods in some way.

You can also create decorators that work with both functions and classes by checking the type of the object being decorated.

import time

def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"Function {func.__name__} took {end_time - start_time:.2f} seconds to run.")
return result
if isinstance(func, type):
# If the decorated object is a class, decorate all its methods
for attr_name in dir(func):
attr = getattr(func, attr_name)
if callable(attr):
setattr(func, attr_name, timer(attr))
return func
else:
# If the decorated object is a function, decorate it directly
return wrapper

@timer
class TestClass:
def method1(self):
time.sleep(1)

def method2(self):
time.sleep(2)

@timer
def my_function():
time.sleep(3)

TestClass().method1()
TestClass().method2()
my_function()

In this example, the timer decorator can be used to decorate both functions and class methods. If the decorated object is a class, the decorator is applied to all its methods using a loop. If the decorated object is a function, the decorator is applied directly using the wrapper function.

Chaining multiple decorators together with decorator ordering

You can also chain multiple decorators together to apply multiple modifications to a function or class. However, the order in which you apply the decorators can be important, since each decorator may modify the behavior of the previous decorators.

def decorator1(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Decorator 1")
result = func(*args, **kwargs)
return result
return wrapper

def decorator2(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Decorator 2")
result = func(*args, **kwargs)
return result
return wrapper

@decorator1
@decorator2
def my_function():
print("Hello, World!")

my_function()

In this example, the decorator2 decorator is applied first, followed by the decorator1 decorator. When the my_function function is called.

This order of decorator application is known as decorator ordering, and it’s important to remember that the decorators are applied in the reverse order of how they appear in the code.

This can be represented in a function call format like this

decorator1(decorator2(my_function))()

This executes the decorator2 function with my_function as an argument, and then passes the result to the decorator1 function. The final result is a decorated function that can be called with the empty parentheses () syntax.

If you found this article informative, please consider following for more insights and tips on Python programming.

Satish Pophale — Medium

Satish Pophale (sattyapatil.github.io)

https://www.linkedin.com/in/satish-pophale

--

--

Satish Pophale
Satish Pophale

Written by Satish Pophale

Tech-savvy Python developer, a lifelong learner and tech enthusiast with passion to solve problems and bring creative solutions to the table.

No responses yet