Python tutorials > Advanced Python Concepts > Decorators > Use cases for decorators?
Use cases for decorators?
Decorators are a powerful feature in Python that allows you to modify or enhance the behavior of functions or methods without changing their core logic. They provide a clean and elegant way to add functionality such as logging, timing, access control, and more. This tutorial explores various use cases for decorators, providing practical examples and explanations to help you understand how to leverage them effectively.
Introduction to Decorators
A decorator is essentially a function that takes another function as an argument, adds some functionality to it, and returns the modified function. This allows you to wrap existing functions with additional behavior in a reusable manner.
Basic Decorator Example
This example demonstrates a simple decorator my_decorator
that wraps the say_hello
function. The @my_decorator
syntax is syntactic sugar for say_hello = my_decorator(say_hello)
. The decorator adds messages before and after the execution of the say_hello
function.
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
Use Case: Logging
This example demonstrates how to use a decorator for logging function calls. The log_calls
decorator logs the function's name, arguments, and return value. The @functools.wraps(func)
decorator is crucial for preserving the original function's metadata (e.g., __name__
, __doc__
).
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args: {args}, kwargs: {kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
@log_calls
def add(x, y):
return x + y
result = add(5, 3)
print(result)
Use Case: Timing
This example showcases how to use a decorator for timing the execution of a function. The timer
decorator measures the time taken by the decorated function and prints it.
import time
import functools
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
execution_time = end_time - start_time
print(f"{func.__name__} took {execution_time:.4f} seconds to execute.")
return result
return wrapper
@timer
def long_running_task():
time.sleep(2)
long_running_task()
Use Case: Access Control
This example illustrates using a decorator for access control. The requires_admin
decorator checks if the 'user' argument is 'admin' before allowing the function to execute. If the user is not an admin, a PermissionError
is raised.
def requires_admin(func):
def wrapper(*args, **kwargs):
user = kwargs.get('user')
if user != 'admin':
raise PermissionError("Admin access required.")
return func(*args, **kwargs)
return wrapper
@requires_admin
def delete_data(data, user=''):
print(f"Deleting data: {data}")
try:
delete_data("sensitive data", user='user1')
except PermissionError as e:
print(e)
delete_data("sensitive data", user='admin')
Use Case: Caching
This example shows how to use a decorator for caching function results. The cache
decorator stores the results of previous function calls and returns the cached result if the same arguments are used again, avoiding redundant calculations.
import functools
def cache(func):
cached_results = {}
@functools.wraps(func)
def wrapper(*args):
if args in cached_results:
return cached_results[args]
else:
result = func(*args)
cached_results[args] = result
return result
return wrapper
@cache
def expensive_operation(n):
print(f"Calculating for {n}")
return n * n
print(expensive_operation(5))
print(expensive_operation(5)) # Returns cached result
Real-Life Use Case: Flask Route Registration
In Flask, the @app.route
decorator is a prime example of how decorators are used in web frameworks. It registers a function as a handler for a specific URL route. Behind the scenes, it modifies the Flask app object to associate the function with the given route.
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello, World!'
Concepts Behind the Snippet
The core concepts behind decorators are:
wrapper
function retains access to the func
argument (the original function) even after the outer function my_decorator
has finished executing.@decorator_name
syntax provides a cleaner and more readable way to apply decorators.
Best Practices
@functools.wraps
: Always use @functools.wraps(func)
inside your decorator's wrapper function. This preserves the original function's metadata (name, docstring, etc.).*args
and **kwargs
).
Interview Tip
When discussing decorators in an interview, highlight their ability to promote code reuse and improve code readability. Explain how they can be used to address common concerns like logging, timing, and authorization in a clean and modular way. Mention the importance of @functools.wraps
for preserving function metadata.
When to Use Them
Use decorators when you need to add functionality to multiple functions or methods in a consistent and reusable way. Common scenarios include:
Memory Footprint
Decorators themselves generally don't introduce a significant memory overhead. The primary memory impact comes from:
cache
example above) can consume memory by storing the results of function calls. This can become a concern if the cached data grows large. Consider using techniques like Least Recently Used (LRU) caching to limit the memory usage.
Alternatives
Alternatives to using decorators include:
with
statements to manage resources or perform setup/teardown actions.
Pros
Cons
FAQ
-
What is `@functools.wraps` and why is it important?
`@functools.wraps(func)` is a decorator that updates the wrapper function to look like the wrapped function. It copies the wrapped function's identity (name, docstring, module, etc.) to the wrapper function. This is crucial for preserving function metadata and ensuring proper introspection.
-
Can I apply multiple decorators to a single function?
Yes, you can apply multiple decorators to a single function. The decorators are applied from top to bottom. For example:
@decorator1 @decorator2 def my_function(): pass
This is equivalent to: `my_function = decorator1(decorator2(my_function))`
-
How do I pass arguments to a decorator?
To pass arguments to a decorator, you need to create a decorator factory. This is a function that returns a decorator. For example:
def repeat(num_times): def decorator_repeat(func): @functools.wraps(func) def wrapper(*args, **kwargs): for _ in range(num_times): result = func(*args, **kwargs) return result return wrapper return decorator_repeat @repeat(num_times=3) def greet(name): print(f"Hello, {name}!") greet("World")