Function Power-Ups in Python: Mastering Decorators and Chaining
Creating Function Decorators
Chaining Decorators
You can chain multiple decorators together by placing them one on top of the other before the function definition. When you do this, the decorators are applied in the order they are written, with the innermost decorator being executed first.
Example: Timing and Debugging Decorators
Here's a common example to illustrate decorators:
def timer(func):
"""Decorator that prints the execution time of a function."""
import time
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs)
end_time = time.perf_counter()
print(f"Function '{func.__name__!r}' took {end_time - start_time:.4f} seconds")
return result
return wrapper
def debug(func):
"""Decorator that prints the function name, arguments and return value."""
def wrapper(*args, **kwargs):
args_repr = [repr(a) for a in args] + [f"{k}={v!r}" for k, v in kwargs.items()]
print(f"Calling '{func.__name__!r}' with {', '.join(args_repr)}")
result = func(*args, **kwargs)
print(f"Function '{func.__name__!r}' returned {result!r}")
return result
return wrapper
@debug # Apply debug decorator first
@timer # Apply timer decorator second
def factorial(n):
"""Calculates the factorial of a number."""
if n == 0:
return 1
else:
return n * factorial(n-1)
# Call the decorated function
factorial(5)
In this example, the debug
decorator prints the function name, arguments, and return value. The timer
decorator calculates and prints the execution time of the decorated function. By chaining them together using the @
symbol, both functionalities are applied to the factorial
function.
This is a basic example of how decorators work. You can create decorators for various purposes like authentication, logging, caching, etc., and chain them together to achieve complex function behavior.
def timer(func):
"""Decorator that prints the execution time of a function."""
import time
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs)
end_time = time.perf_counter()
print(f"Function '{func.__name__!r}' took {end_time - start_time:.4f} seconds")
return result
return wrapper
def debug(func):
"""Decorator that prints the function name, arguments and return value."""
def wrapper(*args, **kwargs):
args_repr = [repr(a) for a in args] + [f"{k}={v!r}" for k, v in kwargs.items()]
print(f"Calling '{func.__name__!r}' with {', '.join(args_repr)}")
result = func(*args, **kwargs)
print(f"Function '{func.__name__!r}' returned {result!r}")
return result
return wrapper
# Chain decorators (debug first, then timer)
@debug
@timer
def factorial(n):
"""Calculates the factorial of a number."""
if n == 0:
return 1
else:
return n * factorial(n-1)
# Call the decorated function
factorial(5)
Explanation:
-
Calling the decorated function: When we call
factorial(5)
, the execution goes through the chained decorators:- The
debug
decorator first prints information about callingfactorial
withn=5
. - Then, the
factorial
function calculates the factorial and returns the result. - Finally, the
debug
decorator prints information about the function returning the result. - After that, the
timer
decorator calculates the execution time and prints it.
- The
This code demonstrates how you can chain decorators to achieve multiple functionalities on a single function.
-
Class-based Decorators:
- Instead of a function, you can define a class that takes the original function as an argument in its constructor.
- Inside the class, define methods for the desired behavior (like pre-processing or post-processing).
- Override the
__call__
method to handle function execution and potentially perform actions before or after. - Use the class instance as the decorator with the
@
symbol.
-
Metaclasses:
- Metaclasses are used to customize the behavior of classes themselves.
- You can define a metaclass that inspects methods within a class definition.
- Based on the method name or other criteria, the metaclass can dynamically modify the method behavior.
- This approach is less common and more complex compared to decorators.
-
Higher-Order Functions (HOFs):
- You can write a function that takes the original function and additional logic as arguments.
- This function can then return a new function that encapsulates the desired behavior.
- While not strictly a decorator, HOFs offer a more flexible approach for complex logic.
class Timer:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
import time
start_time = time.perf_counter()
result = self.func(*args, **kwargs)
end_time = time.perf_counter()
print(f"Function '{self.func.__name__!r}' took {end_time - start_time:.4f} seconds")
return result
@Timer
def factorial(n):
"""Calculates the factorial of a number."""
if n == 0:
return 1
else:
return n * factorial(n-1)
factorial(5)
Remember, decorators are generally preferred due to their concise syntax and readability. These alternatives offer more control but might be less Pythonic for simple use cases.
python function decorator