How do I create function decorators in Python and chain them together?
I need to implement two decorators in Python that would work as follows:
@make_bold
@make_italic
def say():
return "Hello"
When calling say(), it should return:
"<b><i>Hello</i></b>"
Can you provide an implementation for both decorators and explain how they work when chained together?
Function decorators in Python are functions that wrap other functions to modify their behavior without changing their source code. To create and chain decorators, you define wrapper functions that extend the original function’s functionality, then apply them using the @ syntax in reverse order of execution. When decorators are chained, they execute from the innermost (closest to the function) to the outermost, with each decorator’s output becoming the input for the next decorator in the chain.
Contents
- Understanding Decorator Basics
- Creating HTML Formatting Decorators
- Implementing Chained Decorators
- Execution Order and Debugging
- Advanced Decorator Techniques
- Practical Examples and Use Cases
Understanding Decorator Basics
A decorator is a function that takes another function as an argument and extends its behavior without explicitly modifying it. The basic structure of a decorator involves:
- A function that accepts another function as its parameter
- An inner wrapper function that contains the extended behavior
- A return statement that returns the wrapper function
def decorator_name(func):
def wrapper(*args, **kwargs):
# Code to execute before the original function
result = func(*args, **kwargs)
# Code to execute after the original function
return result
return wrapper
The Python programming language uses the @ syntax to apply decorators to functions, making the code more readable and maintainable.
Creating HTML Formatting Decorators
Based on your requirements, let’s implement the make_bold and make_italic decorators that wrap function return values with HTML tags.
The make_bold Decorator
def make_bold(func):
def wrapper():
return f'<b>{func()}</b>'
return wrapper
The make_italic Decorator
def make_italic(func):
def wrapper():
return f'<i>{func()}</i>'
return wrapper
How these work:
- Each decorator defines an inner
wrapperfunction - The
wrappercalls the original function and wraps its return value with HTML tags - The decorator returns the
wrapperfunction, which replaces the original function
According to the official Python documentation, decorators are executed when the module is imported, not when the decorated function is called.
Implementing Chained Decorators
When you chain decorators as shown in your example:
@make_bold
@make_italic
def say():
return "Hello"
The execution order follows a specific pattern:
- Innermost decorator first:
@make_italicis applied first - Outer decorator next:
@make_boldwraps the result ofmake_italic - Final result:
<b><i>Hello</i></b>
Here’s what happens step-by-step:
# Original function
def say():
return "Hello"
# Step 1: Apply @make_italic
say_with_italic = make_italic(say) # Returns wrapper that wraps with <i></i>
# Step 2: Apply @make_bold to the result
say_formatted = make_bold(say_with_italic) # Returns wrapper that wraps with <b></b>
# Final result when called:
say_formatted() # Returns '<b><i>Hello</i></b>'
Key insight: Decorators execute from bottom to top when stacked, but the wrapping happens from inside out.
Complete Implementation
Here’s the complete implementation with the chained decorators:
def make_bold(func):
def wrapper():
return f'<b>{func()}</b>'
return wrapper
def make_italic(func):
def wrapper():
return f'<i>{func()}</i>'
return wrapper
@make_bold
@make_italic
def say():
return "Hello"
# When you call say():
result = say() # Returns '<b><i>Hello</i></b>'
print(result) # Output: <b><i>Hello</i></b>
Execution Order and Debugging
Understanding the execution order is crucial when working with chained decorators. As GeeksforGeeks explains, “Firstly the inner decorator will work and then the outer decorator.”
Visualizing the Execution Flow
Let’s add some debugging to understand what’s happening:
def make_bold(func):
print("Applying bold decorator")
def wrapper():
print("Inside bold wrapper")
result = func()
return f'<b>{result}</b>'
return wrapper
def make_italic(func):
print("Applying italic decorator")
def wrapper():
print("Inside italic wrapper")
result = func()
return f'<i>{result}</i>'
return wrapper
@make_bold
@make_italic
def say():
print("Inside original say() function")
return "Hello"
print("Calling the decorated function:")
result = say()
print(f"Final result: {result}")
Output:
Applying italic decorator
Applying bold decorator
Calling the decorated function:
Inside bold wrapper
Inside italic wrapper
Inside original say() function
Final result: <b><i>Hello</i></b>
Debugging Decorator Execution
The output reveals the execution order:
- Decorators are applied when the module is loaded (prints “Applying…”)
- When the function is called:
- Outermost decorator (
bold) wrapper executes first - Innermost decorator (
italic) wrapper executes next - Original function executes last
- Results are wrapped in reverse order
- Outermost decorator (
Advanced Decorator Techniques
Decorators with Parameters
To make your decorators more flexible, you can add parameters:
def make_html_tag(tag):
def decorator(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return f'<{tag}>{result}</{tag}>'
return wrapper
return decorator
# Usage
@make_html_tag('b')
@make_html_tag('i')
def say(name):
return f"Hello, {name}!"
print(say("Alice")) # Output: <b><i>Hello, Alice!</i></b>
Class-based Decorators
You can also implement decorators using classes:
class BoldDecorator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
return f'<b>{self.func(*args, **kwargs)}</b>'
class ItalicDecorator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
return f'<i>{self.func(*args, **kwargs)}</i>'
@BoldDecorator
@ItalicDecorator
def say():
return "Hello"
print(say()) # Output: <b><i>Hello</i></b>
Preserving Function Metadata
When using decorators, you might lose the original function’s metadata (like __name__, __doc__, etc.). You can preserve this using functools.wraps:
from functools import wraps
def make_bold(func):
@wraps(func)
def wrapper():
return f'<b>{func()}</b>'
return wrapper
def make_italic(func):
@wraps(func)
def wrapper():
return f'<i>{func()}</i>'
return wrapper
@make_bold
@make_italic
def say():
"""Returns a greeting message"""
return "Hello"
print(say.__name__) # Output: say
print(say.__doc__) # Output: Returns a greeting message
Practical Examples and Use Cases
Multiple HTML Tags
You can extend this pattern to create multiple formatting decorators:
def make_bold(func):
def wrapper():
return f'<b>{func()}</b>'
return wrapper
def make_italic(func):
def wrapper():
return f'<i>{func()}</i>'
return wrapper
def make_underline(func):
def wrapper():
return f'<u>{func()}</u>'
return wrapper
@make_bold
@make_italic
@make_underline
def say():
return "Hello"
print(say()) # Output: <b><i><u>Hello</u></i></b>
Logging Decorator
A practical example of decorator chaining is adding logging to a formatted function:
import logging
def log_calls(func):
def wrapper():
logging.info(f"Calling {func.__name__}")
result = func()
logging.info(f"Function {func.__name__} returned: {result}")
return result
return wrapper
@make_bold
@make_italic
@log_calls
def say():
return "Hello"
# This will both format the output and log the function call
result = say()
Performance Monitoring
You can create decorators for performance monitoring and then chain them with formatting:
import time
def timing_decorator(func):
def wrapper():
start_time = time.time()
result = func()
end_time = time.time()
execution_time = end_time - start_time
print(f"Function executed in {execution_time:.4f} seconds")
return result
return wrapper
@make_bold
@timing_decorator
def say():
time.sleep(0.1) # Simulate some work
return "Hello"
result = say()
print(f"Formatted result: {result}")
Conclusion
Key takeaways from this guide:
-
Decorator Basics: Decorators are functions that wrap other functions to modify their behavior without changing the original code.
-
Chaining Mechanics: When chaining decorators, they apply from bottom to top but execute from inside out, creating a nested wrapping effect.
-
HTML Formatting: The
make_boldandmake_italicdecorators demonstrate how to wrap function return values with HTML tags like<b></b>and<i></i>. -
Order Matters: The order in which you chain decorators significantly affects the final output, as each decorator wraps the result of the one below it.
-
Advanced Techniques: You can enhance decorators with parameters, class-based implementations, and metadata preservation using
functools.wraps.
For further exploration, consider experimenting with different tag combinations, adding error handling to decorators, or creating more complex formatting scenarios. The decorator pattern is a powerful tool in Python that can significantly improve code organization and reusability.
Sources
- How do I make function decorators and chain them together? - Stack Overflow
- Chain Multiple Decorators in Python - GeeksforGeeks
- Python: All About Decorators - DZone
- Python Exercise: Make a chain of function decorators in Python - w3resource
- Creating and Chaining Function Decorators in Python - Medium
- Python: how to use multiple decorators on one function - Teclado Blog
- Mastering Python Decorators: Chaining and Order of Execution - LinkedIn
- Advanced Lesson 4: Python Decorators > Chaining decorators - Imperial College London