Understanding the Basics: Decorators without Arguments
Before diving into arguments, let’s briefly review the fundamental concept of a decorator. A decorator is essentially a function that takes another function as input and returns a modified version of that 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()
This code defines a decorator my_decorator
that prints messages before and after the execution of the decorated function say_hello
. The @my_decorator
syntax is syntactic sugar for say_hello = my_decorator(say_hello)
.
Adding Arguments to Your Decorators
The key to creating decorators with arguments lies in adding another layer of function nesting. The outer function accepts the arguments, while the inner function (the actual decorator) receives the original function.
def repeat(num_times):
def decorator_repeat(func):
def wrapper(*args, **kwargs):
for _ in range(num_times):
= func(*args, **kwargs)
result return result
return wrapper
return decorator_repeat
@repeat(num_times=3)
def greet(name):
print(f"Hello, {name}!")
"World") greet(
In this example, repeat
is a decorator factory. It takes num_times
as an argument and returns the actual decorator decorator_repeat
. decorator_repeat
then wraps the function greet
, executing it multiple times. The *args
and **kwargs
allow the decorator to handle functions with any number of positional or keyword arguments.
More Complex Examples: Decorators with Arguments and Variable Behavior
Let’s explore a more sophisticated scenario: a decorator that times the execution of a function.
import time
def timing(func):
def wrapper(*args, **kwargs):
= time.time()
start = func(*args, **kwargs)
result = time.time()
end print(f"Execution time: {end - start:.4f} seconds")
return result
return wrapper
@timing
def slow_function(n):
time.sleep(n)return n*2
2) slow_function(
This timing
decorator measures and prints the execution time of the decorated function. Notice how it seamlessly handles functions with varying argument numbers and types.
Using functools.wraps for Improved Debugging
When debugging decorated functions, it’s beneficial to preserve the original function’s metadata (name, docstring, etc.). The functools.wraps
decorator helps achieve this.
import functools
import time
def timing(func):
@functools.wraps(func) #Preserves function metadata
def wrapper(*args, **kwargs):
= time.time()
start = func(*args, **kwargs)
result = time.time()
end print(f"Execution time: {end - start:.4f} seconds")
return result
return wrapper
@timing
def slow_function(n):
time.sleep(n)return n*2
2) slow_function(
By incorporating functools.wraps
, you improve the readability and debuggability of your code significantly. Without it, the decorated function’s metadata would be replaced by that of the wrapper function.
Practical Applications of Decorators with Arguments
Decorators with arguments are invaluable for a wide range of tasks, including:
- Authentication and Authorization: Controlling access to functions based on user roles or permissions.
- Logging and Monitoring: Tracking function calls, execution times, and error handling.
- Caching: Storing and reusing function results to improve performance.
- Input Validation: Ensuring that function arguments meet specific criteria before execution.
By mastering decorators with arguments, you enhance your Python skills and write more concise, maintainable, and reusable code.