Skip to main content

Advanced Decorators: Arguments, Chaining & Classes

Once you understand basic function decorators, you will encounter scenarios where you need more power:

  1. Passing custom configuration parameters into decorators (e.g., @repeat(num_times=3) or @cache(ttl=60)).
  2. Applying multiple decorators on a single function (Chaining).
  3. 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.

Execution Order

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)!