Understanding the Basics
At its core, a decorator is a function that takes another function as input and returns a modified version of that function. This modification can involve adding functionality before, after, or even around the original function’s execution.
Let’s start with a simple example:
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
. The @my_decorator
syntax above say_hello()
is syntactic sugar; it’s equivalent to say_hello = my_decorator(say_hello)
. The output demonstrates that the wrapper function executes code before and after the original say_hello()
function.
Decorators with Arguments
Decorators can also handle functions that accept arguments. To achieve this, the wrapper function needs to accept the same arguments as the original function and pass them along:
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(
Here, repeat
is a decorator factory – it returns a decorator. The *args
and **kwargs
allow the wrapper to handle any number of positional and keyword arguments passed to the decorated function.
Decorators with Return Values
Modifying the return value of the decorated function is straightforward:
def make_bold(func):
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
@make_bold
def get_message():
return "Hello, world!"
print(get_message()) # Output: <b>Hello, world!</b>
This example shows how to wrap the return value of the function with HTML bold tags.
Practical Applications
Decorators are invaluable for various tasks, including:
- Logging: Record function calls and their arguments.
- Timing: Measure the execution time of a function.
- Authentication: Control access to functions based on user permissions.
- Caching: Store the results of expensive function calls to improve performance.
- Input validation: Sanitize or validate input before passing it to the function.
Let’s illustrate logging with a decorator:
import functools
def log_calls(func):
@functools.wraps(func) #Preserves metadata of original function
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with arguments: {args}, {kwargs}")
= func(*args, **kwargs)
result print(f"{func.__name__} returned: {result}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
5, 3) add(
Note the use of functools.wraps
. This is crucial for preserving the original function’s metadata (like name and docstring) after decoration. Without it, the decorated function would lose its original identity.