27.Decorators
Decorators in Python are a powerful tool that allow you to modify the behavior of functions or classes. They are commonly used for logging, enforcing access control, instrumentation, caching, and more.
Comprehensive Explanation
A decorator is a function that takes another function as an argument and extends or alters its behavior without explicitly modifying it. This is useful for adhering to the DRY (Don’t Repeat Yourself) principle and for abstracting repetitive tasks like logging, timing, or access control.
Syntax and Multiple Examples
Basic Decorator Syntax:
@decorator_function
def function_to_decorate():
pass
Example 1: Logging Decorator
def log_decorator(func):
def wrapper(*args, **kwargs):
print(f”Calling {func.__name__} with arguments {args} and {kwargs}”)
result = func(*args, **kwargs)
print(f”{func.__name__} returned {result}”)
return result
return wrapper
@log_decorator
def add(a, b):
return a + b
add(2, 3)
Example 2: Timing Decorator
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f”Execution time: {end – start} seconds”)
return result
return wrapper
@timer
def slow_function():
time.sleep(2)
return “Done”
slow_function()
Use Cases
– Logging function calls and results
– Measuring execution time
– Access control and authentication
– Memoization and caching
– Input validation
– Debugging and profiling
Types of Decorators
Function Decorators
Function decorators are the most common type. They wrap a function and modify its behavior. They are defined using the ‘@’ symbol followed by the decorator function name.
Class Decorators
Class decorators are similar to function decorators but are applied to classes. They can be used to modify or enhance class behavior. For example, you can use a class decorator to register classes or add methods dynamically.
Chaining Decorators
Multiple decorators can be applied to a single function by stacking them. The decorators are applied from the innermost to the outermost.
@decorator_one
@decorator_two
def my_function():
pass
In this example, ‘decorator_two’ is applied first, then ‘decorator_one’.
Best Practices
– Use functools.wraps to preserve metadata of the original function
– Keep decorators simple and focused on a single responsibility
– Avoid side effects in decorators unless necessary
– Document your decorators clearly
Common Pitfalls
– Forgetting to use functools.wraps can lead to loss of function metadata
– Overusing decorators can make code harder to read and debug
– Not handling arguments properly in the wrapper function
– Applying decorators in the wrong order when chaining