7.6 - Decorators
7.6.1 - Introduction
Python decorators are versatile functions that enhance or modify other functions, all without altering their fundamental code structure. They embody the DRY (Don't Repeat Yourself) principle, promoting code reuse and contributing to cleaner, more efficient programming. With decorators, you can seamlessly adjust function behavior, making your code more readable and maintainable. Their extensive use across Python's libraries and frameworks, especially for tasks like logging and access control, underscores their importance. Gaining proficiency in using decorators is a key step towards mastering Python, enabling you to develop code that's not only powerful but also elegantly structured.
7.6.2 - Basic Syntax of Decorators
In Python, decorators use the @
symbol and are placed above the function definition. The basic syntax is:
@decorator
def function():
# function body
7.6.3 - Simple Decorator Example
Let's start with a simple example to illustrate how decorators work:
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()
When you run this code, it will output:
Something is happening before the function is called.
Hello!
Something is happening after the function is called.
7.6.4 - Decorators with Parameters
Decorators can also accept parameters. Here's an example:
def repeat(n):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(n):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
greet("World")
This will print "Hello, World!" three times.
7.6.5 - Using functools.wraps
When you use a decorator, the original function's metadata (like its name, docstring, etc.) gets changed to that of the wrapper. To avoid this, use functools.wraps
:
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# do something before
result = func(*args, **kwargs)
# do something after
return result
return wrapper
7.6.6 - When to Use Decorators
- Logging: To add logging functionality to track the execution of functions.
- Timing: To measure the time taken by a function to execute.
- Authorization: To check whether a user has the right permissions to execute a function.
- Caching: To cache the results of a function.
- Validation: To validate the input passed to a function.
7.6.7 - Decorators Case Studies
7.6.7.1 - Case Study 1: Logging Decorator
In this example, we'll create a decorator that logs the execution of a function, including its name and the arguments it was called with.
import logging
from functools import wraps
def log_function_call(func):
@wraps(func)
def wrapper(*args, **kwargs):
logging.info(f"Running '{func.__name__}' with arguments {args} and keyword arguments {kwargs}")
return func(*args, **kwargs)
return wrapper
# Example usage
@log_function_call
def add(x, y):
return x + y
# Set up logging
logging.basicConfig(level=logging.INFO)
# Test the function
result = add(5, 10)
When you run this code, it will log information about the add
function being called.
7.6.7.2 - Case Study 2: Performance Timer Decorator
This decorator measures the time a function takes to execute, which is helpful for performance testing.
import time
from functools import wraps
def timing_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"Function '{func.__name__}' took {end_time - start_time:.4f} seconds to execute.")
return result
return wrapper
# Example usage
@timing_decorator
def long_running_function(seconds):
time.sleep(seconds)
# Test the function
long_running_function(2)
This will print the time taken by long_running_function
to execute.
7.6.7.3 - Case Study 3: Authorization Decorator
This decorator checks if a user has the correct role to execute a function. It's a basic example of how decorators can be used for access control.
from functools import wraps
current_user = {"username": "john", "role": "admin"}
def role_required(role):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
if current_user.get("role") == role:
return func(*args, **kwargs)
else:
raise PermissionError("You do not have the right role to access this function.")
return wrapper
return decorator
# Example usage
@role_required("admin")
def sensitive_function():
return "Sensitive Data"
# Test the function
try:
data = sensitive_function()
print(data)
except PermissionError as e:
print(str(e))
In this case, only users with the role "admin" can call sensitive_function
.
Certainly! Here are two additional case studies demonstrating the use of decorators in Python 3.12.
7.6.7.4 - Case Study 4: Caching/Memoization Decorator
This example showcases a decorator for caching the results of a function. This is particularly useful for expensive computational functions where you want to avoid recalculating results for previously processed inputs.
from functools import wraps
def cache_decorator(func):
cache = {}
@wraps(func)
def wrapper(*args):
if args in cache:
return cache[args]
else:
result = func(*args)
cache[args] = result
return result
return wrapper
# Example usage
@cache_decorator
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
# Test the function
print(fibonacci(10)) # This will be cached for subsequent calls
This decorator stores the results of the fibonacci
function calls in a cache, so repeated calls with the same arguments will return instantly from the cache.
7.6.7.5 - Case Study 5: Validation Decorator
In this example, we'll create a decorator that validates the input arguments of a function. This can be particularly useful for functions that require specific types or value ranges for their arguments.
from functools import wraps
def validate_types(*arg_types):
def decorator(func):
@wraps(func)
def wrapper(*args):
if len(args) != len(arg_types):
raise ValueError("Invalid number of arguments")
for a, t in zip(args, arg_types):
if not isinstance(a, t):
raise TypeError(f"Argument {a} is not of type {t}")
return func(*args)
return wrapper
return decorator
# Example usage
@validate_types(int, int)
def add(a, b):
return a + b
# Test the function
print(add(2, 3)) # This works fine
# print(add("a", "b")) # This raises a TypeError
This decorator ensures that the add
function is only called with integer arguments. If not, it raises a TypeError
.