Python tutorials > Advanced Python Concepts > Decorators > How to write reusable decorators?
How to write reusable decorators?
Basic Decorator Structure
my_decorator
function takes a function func
as input and returns a wrapper function. The wrapper function executes code before and after calling the original function. The @my_decorator
syntax is syntactic sugar for say_hello = my_decorator(say_hello)
. The *args
and **kwargs
are essential for handling functions with any number of positional and keyword arguments, making the decorator more versatile. The result = func(*args, **kwargs)
line actually calls the original function. Returning result
ensures the original function's return value is preserved.
def my_decorator(func):
def wrapper(*args, **kwargs):
# Code to be executed before calling the function
print("Before function call")
result = func(*args, **kwargs)
# Code to be executed after calling the function
print("After function call")
return result
return wrapper
@my_decorator
def say_hello(name):
print(f"Hello, {name}!")
say_hello("Alice")
Understanding the Need for Reusability
Using Decorator Factories
repeat(num_times)
doesn't directly return a wrapper function; it returns another function (decorator_repeat
) that then returns the wrapper. This allows you to parameterize the decorator. The num_times
argument controls how many times the decorated function is executed. Using a decorator factory lets you create multiple variations of the decorator based on different configurations.
def repeat(num_times):
def decorator_repeat(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("Bob")
Preserving Function Metadata with functools.wraps
@functools.wraps
, the decorated function (say_hello
) would lose its original name (__name__
) and docstring (__doc__
). functools.wraps
copies the original function's metadata to the wrapper, preserving important information for introspection and debugging. Always use @functools.wraps
when defining decorators to ensure proper behavior. This is crucial for debugging and documentation generation.
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("Before function call")
result = func(*args, **kwargs)
print("After function call")
return result
return wrapper
@my_decorator
def say_hello(name):
"""Says hello to the given name."""
print(f"Hello, {name}!")
print(say_hello.__name__)
print(say_hello.__doc__)
Concepts Behind the Snippet
wrapper
, decorator_repeat
) have access to variables from their enclosing scope (e.g., num_times
in the repeat
example).my_decorator(func)
) and returned as values.@my_decorator
syntax is a shorthand way to apply a decorator.*args
and **kwargs
for maximum flexibility.
Real-Life Use Case: Logging Function Calls
log_calls
decorator factory takes a logger object as an argument, allowing you to customize the logging behavior. It then logs a message before and after calling the function, including the arguments and return value. This can be invaluable for debugging and auditing. The logging
module provides flexible ways to configure log levels and output destinations. The !r
in the f-strings is used to get the 'repr' (representation) of the result for debugging.
import functools
import logging
logging.basicConfig(level=logging.INFO)
def log_calls(logger):
def decorator_log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
logger.info(f"Calling {func.__name__}({signature})")
result = func(*args, **kwargs)
logger.info(f"{func.__name__} returned {result!r}")
return result
return wrapper
return decorator_log_calls
logger = logging.getLogger(__name__)
@log_calls(logger)
def add(x, y):
return x + y
add(2, 3)
Best Practices
@functools.wraps
: Always preserve function metadata.
Interview Tip
@functools.wraps
and decorator factories. Being able to explain how decorators work behind the scenes is also beneficial. Mentioning common use cases like logging, timing, and authentication will further impress the interviewer.
When to Use Them
Memory Footprint
Alternatives
The best approach depends on the specific problem and the desired level of code reusability.
Pros
Cons
FAQ
-
What is the purpose of
functools.wraps
?
functools.wraps
is used to preserve the original function's metadata (name, docstring, etc.) when using decorators. Without it, the decorated function will lose its original attributes. -
How can I pass arguments to a decorator?
You can use a decorator factory, which is a function that returns a decorator. This allows you to pass arguments to the outer function, which are then available to the decorator. -
Are decorators always the best solution?
No, decorators are not always the best solution. They are most suitable for cross-cutting concerns and code reuse. In some cases, other approaches like function composition or mixins may be more appropriate.