Advanced Decorators: Arguments, Chaining & Classes
Once you understand basic function decorators, you will encounter scenarios where you need more power:
- Passing custom configuration parameters into decorators (e.g.,
@repeat(num_times=3)or@cache(ttl=60)). - Applying multiple decorators on a single function (Chaining).
- Writing Class-Based Decorators to preserve state across multiple function calls.
1. Decorators that Accept Arguments
To pass arguments directly into a decorator, you add one extra layer of nesting (a 3-level deep function factory).
Real-World Example: An Automated Retry Decorator
import time
from functools import wraps
def repeat(num_times):
# Outer level: Accepts custom configuration arguments
def decorator_factory(original_func):
@wraps(original_func)
def wrapper(*args, **kwargs):
last_result = None
for attempt in range(1, num_times + 1):
print(f"[Attempt {attempt}/{num_times}] Calling {original_func.__name__}...")
last_result = original_func(*args, **kwargs)
return last_result
return wrapper
return decorator_factory
# Using the configurable decorator
@repeat(num_times=3)
def send_notification(user):
print(f"Pinged message to {user}!")
send_notification("Rahul")
2. Chaining Multiple Decorators
You can stack multiple decorators on top of a single function.
Decorators are applied from the bottom up (closest to the function first) and executed from the top down.
from functools import wraps
def bold(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
def italic(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<i>{func(*args, **kwargs)}</i>"
return wrapper
# Applied bottom-up: italic first, then bold
@bold
@italic
def format_announcement(message):
return message
result = format_announcement("Flash Sale 50% Off!")
print(result) # Output: <b><i>Flash Sale 50% Off!</i></b>
3. Class-Based Decorators (Preserving State)
If a decorator needs to remember state across multiple function calls (such as counting how many times an API was requested or rate-limiting users), using a Python Class with the __call__ dunder method is much cleaner than global variables:
class CallCounter:
def __init__(self, original_func):
self.original_func = original_func
self.call_count = 0 # State remembered across calls
def __call__(self, *args, **kwargs):
self.call_count += 1
print(f"📊 Function '{self.original_func.__name__}' called {self.call_count} times.")
return self.original_func(*args, **kwargs)
@CallCounter
def process_payment(amount):
return f"Payment of ₹{amount} processed."
process_payment(500)
process_payment(1200)
process_payment(300)
# Output:
# 📊 Function 'process_payment' called 1 times.
# 📊 Function 'process_payment' called 2 times.
# 📊 Function 'process_payment' called 3 times.
4. Built-in Production Decorators in Python
Python's standard library comes with several pre-built decorators that every engineer should know:
@property
Turns a method into a read-only attribute getter with encapsulation.
@functools.lru_cache
Automatically caches expensive calculation results in memory (memoization).
@classmethod
Passes the class (cls) instead of an instance (self) into methods.
@staticmethod
Creates independent utility methods that don't need access to class or instance state.
Quick Summary
- Decorators with Arguments: Uses a 3-layer nested function factory to accept configuration parameters.
- Stacking Order: Multiple decorators execute bottom-to-top (inside-out) closest to the function first.
- Class Decorators (
__call__): Useful when decorators need to remember state (rate limits, request counts). - Built-in Essentials:
@property(getters/setters),@lru_cache(memoization),@classmethod, and@staticmethod.
What's Next?
Now let's explore powerful text pattern matching, string validation, and data extraction using Module 19: Regular Expressions (RegEx)!