Introduction
In this lesson, you'll advance your understanding of Python decorators by learning how to write reusable decorators for logging, timing, and caching, thus further enhancing the functionality and performance of your Python functions. You'll gain hands-on experience implementing these decorators and learn how to compose them safely while preserving original function metadata.
Implementing Basic Function Decorators
Decorators are a powerful feature in Python that allows you to modify the behavior of a function or class method. They use the concept of functions that return other functions and can be applied using the @decorator syntax.
Here's a simple example of a decorator that prints a message before and after a function execution:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("Before the function call")
result = func(*args, **kwargs)
print("After the function call")
return result
return wrapper
@my_decorator
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
# Output
Before the function call
Hello, Alice!
After the function call
Writing a Reusable Logging Decorator
A logging decorator can be a useful tool for tracking function calls and their arguments, which is particularly handy during debugging.
import functools
# Logging decorator
def log_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Log function call
print(f"Calling {func.__name__} with args: {args}, kwargs: {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(5, 7)
# Output
Calling add with args: (5, 7), kwargs: {}
add returned 12
Creating a Timing Decorator
To measure how long a function runs, a timing decorator can be implemented using Python's time module.
import time
import functools
# Timing decorator
def timer_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs)
end_time = time.perf_counter()
# Log execution time
print(f"{func.__name__} took {end_time - start_time:.4f} seconds")
return result
return wrapper
@timer_decorator
def calculate_sum(n):
total = sum(range(n))
return total
calculate_sum(100000)
# Output
calculate_sum took 0.0054 seconds
Building a Caching Decorator
Caching can enhance performance by avoiding redundant function executions. This can be achieved by storing the results of expensive function calls.
import functools
# Caching decorator
def cache_decorator(func):
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args in cache:
return cache[args]
result = func(*args)
cache[args] = result
return result
return wrapper
@cache_decorator
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
fib(10)
# Output
Manages internal cache to prevent recomputation
Composing Multiple Decorators Safely
Composing multiple decorators requires awareness of how they alter function metadata and stack composition. Utilize functools.wraps to preserve function metadata, such as its name and docstring.
@timer_decorator
@log_decorator
def multiply(x, y):
return x * y
multiply(3, 4)
# Output
Calling multiply with args: (3, 4), kwargs: {}
multiply returned 12
multiply took 0.0000 seconds
Summary / Key Takeaways
In this lesson on writing reusable decorators, you have learned:
- Implement Basic Decorators: Use nested functions and the
@decoratorsyntax to alter function behavior. - Logging Decorator: Track function calls, arguments, and results for ease in debugging.
- Timing Decorator: Measure function execution time to optimize performance.
- Caching with Decorators: Use caching to store function results and prevent unnecessary recomputation.
- Compose Safely: Stack multiple decorators using
functools.wrapsto maintain function integrity and metadata.
