10.5 Decorators in Python
In Python, decorators are a powerful and flexible tool that allow you to modify or enhance the behavior of functions or methods. A decorator is essentially a function that takes another function as an argument, adds some functionality to it, and returns a new function. Decorators are commonly used for cross-cutting concerns like logging, timing, memoization, and authorization, without modifying the original function’s code.
In this section, we will explore how decorators work, how to create and apply them, and some common use cases.
10.5.1 What is a Decorator?
A decorator is a higher-order function that takes a function (or method) as input, extends its behavior, and returns a modified version of that function. Decorators are often applied using the @decorator_name
syntax, which makes them easy to use and read.
How Decorators Work:
- A decorator is a function that takes another function as an argument.
- It defines a new function that extends the behavior of the original function.
- It returns the new function, which can then be used in place of the original.
Basic Example: A Simple Decorator:
def my_decorator(func):
def wrapper():
print("Something before the function")
func()
print("Something after the function")
return wrapper
# Applying the decorator manually
def say_hello():
print("Hello!")
decorated_say_hello = my_decorator(say_hello)
decorated_say_hello()
Output:
Something before the function
Hello!
Something after the function
In this example:
my_decorator()
takes thesay_hello()
function as input and extends its behavior by printing messages before and after the original function is called.- The decorated function
decorated_say_hello()
now has the additional behavior, while still callingsay_hello()
internally.
10.5.2 Applying a Decorator with the @
Syntax
Python provides a convenient syntax for applying decorators using the @
symbol. Instead of manually calling the decorator, you can apply it directly to the function.
Example: Applying a Decorator with @
:
def my_decorator(func):
def wrapper():
print("Before the function")
func()
print("After the function")
return wrapper
@my_decorator # Applying the decorator
def say_hello():
print("Hello!")
# Call the decorated function
say_hello()
Output:
Before the function
Hello!
After the function
In this example:
- The
@my_decorator
syntax applies the decorator directly to thesay_hello()
function, so it is automatically wrapped when called.
10.5.3 Decorators with Arguments
Sometimes, you need to create decorators that can accept arguments. In this case, you can define a decorator factory that takes arguments and returns a decorator.
Example: Decorator with Arguments:
def repeat(num_times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(num_times):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3) # The function will be repeated 3 times
def say_hello():
print("Hello!")
# Call the decorated function
say_hello()
Output:
Hello!
Hello!
Hello!
In this example:
- The
repeat()
function is a decorator factory that accepts the number of times the function should be repeated. - The decorator
decorator()
is applied to thesay_hello()
function, repeating it three times when called.
10.5.4 Decorating Functions with Arguments
If the function being decorated accepts arguments, you can modify the wrapper function to accept any number of positional and keyword arguments using *args
and **kwargs
.
Example: Decorating Functions with Arguments:
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Before the function")
result = func(*args, **kwargs)
print("After the function")
return result
return wrapper
@my_decorator
def add(x, y):
return x + y
# Call the decorated function
result = add(5, 3)
print(f"Result: {result}")
Output:
Before the function
After the function
Result: 8
In this example:
- The decorator
my_decorator()
is applied to theadd()
function, which accepts two arguments. - The
*args
and**kwargs
in the wrapper allow the decorator to handle functions with any number of arguments.
10.5.5 Common Use Cases for Decorators
Decorators are commonly used for a variety of tasks in Python programming. Below are some typical use cases:
1. Logging
Decorators can be used to automatically log the execution of functions.
def log_decorator(func):
def wrapper(*args, **kwargs):
print(f"Calling function {func.__name__} with arguments {args}, {kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
@log_decorator
def add(x, y):
return x + y
add(10, 20)
Output:
Calling function add with arguments (10, 20), {}
add returned 30
2. Timing Functions
You can use decorators to measure how long a function takes to execute.
import time
def timing_decorator(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{func.__name__} took {end_time - start_time:.6f} seconds")
return result
return wrapper
@timing_decorator
def slow_function():
time.sleep(1) # Simulate a slow function
slow_function()
Output:
slow_function took 1.000124 seconds
3. Memoization (Caching)
Decorators can be used for memoization, which caches the results of expensive function calls to avoid redundant computations.
def memoize_decorator(func):
cache = {}
def wrapper(n):
if n not in cache:
cache[n] = func(n)
return cache[n]
return wrapper
@memoize_decorator
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(10)) # Output: 55
In this example:
- The decorator caches the results of
fibonacci()
to avoid recomputing the same values, speeding up subsequent calls.
10.5.6 Stacking Multiple Decorators
You can apply multiple decorators to a single function by stacking them. Each decorator is applied in sequence, starting from the innermost (bottom) to the outermost (top).
Example: Stacking Multiple Decorators:
def decorator1(func):
def wrapper(*args, **kwargs):
print("Decorator 1 before")
result = func(*args, **kwargs)
print("Decorator 1 after")
return result
return wrapper
def decorator2(func):
def wrapper(*args, **kwargs):
print("Decorator 2 before")
result = func(*args, **kwargs)
print("Decorator 2 after")
return result
return wrapper
@decorator1
@decorator2
def say_hello():
print("Hello!")
# Call the function with stacked decorators
say_hello()
Output:
Decorator 1 before
Decorator 2 before
Hello!
Decorator 2 after
Decorator 1 after
In this example:
decorator2()
is applied first (closer to the function), followed bydecorator1()
, and they execute in sequence.
10.5.7 Preserving Function Metadata with functools.wraps
When you apply a decorator to a function, the metadata of the original function (such as its name and docstring) can be lost because the decorator replaces the original function with the wrapper function. To preserve this metadata, you can use the functools.wraps()
decorator.
Example: Preserving Metadata with functools.wraps
:
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Before the function")
result = func(*args, **kwargs)
print("After the function")
return result
return wrapper
@my_decorator
def greet(name):
"""Greets the person by name."""
print(f"Hello, {name}!")
print(greet.__name__) # Output: greet
print(greet.__doc__) # Output: Greets the person by name.
In this example:
- The **`@wraps(func
)** decorator ensures that the metadata of the **
greet()`** function (like its name and docstring) is preserved when it’s decorated.
10.5.8 Summary
- Decorators: A function that wraps another function to extend or modify its behavior.
@decorator
Syntax: A convenient way to apply a decorator to a function or method.- Decorators with Arguments: You can create decorators that accept arguments by defining a decorator factory.
- Common Use Cases: Logging, timing, memoization, and access control are common applications of decorators.
- Stacking Decorators: You can apply multiple decorators to a function by stacking them.
functools.wraps
: A decorator used to preserve the metadata of the original function when decorating it.
Decorators are a powerful tool for creating clean, reusable, and modular code in Python, enabling you to modify or extend the behavior of functions without altering their internal logic.