Mastering Decorators in Python: A Comprehensive Guide with Examples
Decorate your code with Python decorators to enhance its functionality, reusability, and aesthetics
- Introduction to decorators
- Function decorators
- Class decorators
- Decorators with
__call__
method - Preserving metadata with
wraps
- Creating argumented decorators
- Creating decorator that work with both function and classes
- 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.