Python - Decorators
functools.wraps
decorator in Python? In Python, the functools.wraps
decorator is used to preserve the original function's metadata, such as its name, docstring, and other attributes, when creating a decorator. It helps in maintaining clarity and consistency in the code, especially when multiple decorators are applied to a function.
Here's an example to illustrate the purpose of functools.wraps
:
import functools
# Decorator without functools.wraps
def decorator_without_wraps(func):
def wrapper():
"""Wrapper function without wraps."""
print(f"Calling {func.__name__}")
func()
return wrapper
# Decorator with functools.wraps
def decorator_with_wraps(func):
@functools.wraps(func)
def wrapper():
"""Wrapper function with wraps."""
print(f"Calling {func.__name__}")
func()
return wrapper
# Applying decorators
@decorator_without_wraps
def function1():
"""Original function without wraps."""
print("Function 1")
@decorator_with_wraps
def function2():
"""Original function with wraps."""
print("Function 2")
# Output metadata
output_metadata1 = f"Function 1: {function1.__name__}, Docstring: {function1.__doc__}"
output_metadata2 = f"Function 2: {function2.__name__}, Docstring: {function2.__doc__}"
In this example:
-
decorator_without_wraps
is a decorator without usingfunctools.wraps
. -
decorator_with_wraps
is a decorator usingfunctools.wraps
to preserve the original function's metadata. -
@decorator_without_wraps
and@decorator_with_wraps
are applied tofunction1
andfunction2
respectively.
When you run this program, you will get the following output:
Calling wrapper Function 1: wrapper, Docstring: Wrapper function without wraps. Calling function2 Function 2: function2, Docstring: Original function with wraps.
Without functools.wraps
, the metadata of the original function is not preserved, resulting in the decorator's wrapper function being displayed in the output. Using functools.wraps
ensures that the original function's metadata is maintained, improving the clarity of the code.
Decorators in Python offer a powerful and flexible way to modify or extend the behavior of functions or methods. They are versatile and can be used in various scenarios to enhance code readability, maintainability, and reusability.
Here are some potential use cases for decorators:
-
Logging: Adding logging statements before and after function calls.
def log_function_call(func): def wrapper(*args, **kwargs): print(f"Calling {func.__name__} with arguments {args} and keyword arguments {kwargs}") result = func(*args, **kwargs) print(f"{func.__name__} returned {result}") return result return wrapper @log_function_call def add(a, b): return a + b # Using the decorated function result = add(3, 5)
Output:
Calling add with arguments (3, 5) and keyword arguments {} add returned 8
-
Timing: Measuring the execution time of functions.
import time def timing(func): def wrapper(*args, **kwargs): start_time = time.time() result = func(*args, **kwargs) end_time = time.time() print(f"{func.__name__} took {end_time - start_time:.2f} seconds to execute.") return result return wrapper @timing def slow_function(): time.sleep(2) print("Function executed.") # Using the decorated function slow_function()
Output:
Function executed. slow_function took 2.00 seconds to execute.
-
Authorization: Checking user permissions before allowing function execution.
def check_permission(func): def wrapper(user, *args, **kwargs): if user.is_admin: result = func(user, *args, **kwargs) else: result = "Permission denied." return result return wrapper @check_permission def admin_only_function(user): return f"Welcome, {user.username}! Admin privileges granted." # Using the decorated function user1 = {'username': 'Admin', 'is_admin': True} result = admin_only_function(user1)
Output:
Welcome, Admin! Admin privileges granted.
Decorators enhance code readability and reusability by promoting a modular and clean code structure. They allow developers to separate concerns, improve the organization of code, and encapsulate common functionalities. Let's explore these benefits with an example:
# Decorator for logging
def log_function_call(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with arguments {args} and keyword arguments {kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
# Decorator for timing
def timing(func):
import time
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{func.__name__} took {end_time - start_time:.2f} seconds to execute.")
return result
return wrapper
# Original function
@log_function_call
@timing
def add(a, b):
return a + b
# Using the decorated function
result = add(3, 5)
In this example:
-
Readability: Decorators allow developers to express the intent and behavior of a function clearly without cluttering its implementation. In this case, the
@log_function_call
and@timing
decorators provide logging and timing functionality respectively, making the code self-explanatory. -
Reusability: Decorators encapsulate common functionalities, making them reusable across different functions. For example, the
log_function_call
andtiming
decorators can be applied to various functions without duplicating the logging and timing code in each function.
Output:
Calling add with arguments (3, 5) and keyword arguments {} add took 0.00 seconds to execute. add returned 8
By using decorators, the original function add
remains focused on its core logic, while the decorators handle additional concerns. This separation of concerns improves code readability and makes it easier to maintain and reuse functionalities across different parts of the codebase.
In Python, decorators can be implemented using either functions or classes. Both approaches achieve the same goal of extending or modifying the behavior of functions, but they differ in their syntax and the way they handle the wrapping of functions.
Here's an example to illustrate the difference between class-based and function-based decorators:
# Function-based decorator
def function_decorator(func):
def wrapper(*args, **kwargs):
print("Function Decorator: Before function is called.")
result = func(*args, **kwargs)
print("Function Decorator: After function is called.")
return result
return wrapper
# Class-based decorator
class ClassDecorator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print("Class Decorator: Before function is called.")
result = self.func(*args, **kwargs)
print("Class Decorator: After function is called.")
return result
# Applying function-based decorator
@function_decorator
def function_example():
print("Inside the original function.")
# Applying class-based decorator
@ClassDecorator
def class_example():
print("Inside the original function.")
# Using decorated functions
function_example()
class_example()
In this example:
-
Function-based Decorator: The
function_decorator
is a function that takes a function as an argument, wraps it, and returns the wrapper function. The decorator is applied using the@
syntax. -
Class-based Decorator: The
ClassDecorator
is a class with a__init__
method to initialize the instance with the original function, and a__call__
method to define the behavior of the wrapper function. The decorator is applied using the@
syntax as well.
Output:
Function Decorator: Before function is called. Inside the original function. Function Decorator: After function is called. Class Decorator: Before function is called. Inside the original function. Class Decorator: After function is called.
Both approaches achieve the same result, but class-based decorators offer additional opportunities for encapsulation and maintaining state across multiple calls since they can have instance variables. Function-based decorators are simpler and more concise, making them a common choice for many use cases.