DEV Community

Cover image for Python by Structure - Class-Based Decorators That Remember
Aaron Rose
Aaron Rose

Posted on

Python by Structure - Class-Based Decorators That Remember

Timothy was reviewing a performance monitoring system when he stopped on an unfamiliar pattern. "Margaret, this decorator isn't a function - it's a class. I've only ever seen decorators written as functions."

Margaret looked over. "Class-based decorators are powerful when you need your decorator to maintain state. What are you looking at?"

class CallCounter:
    """A decorator class to count function calls."""
    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"-> Calling {self.func.__name__}. Call count: {self.count}")
        return self.func(*args, **kwargs)

@CallCounter
def process_data(data):
    """Processes the input data."""
    return data.upper()

print(process_data("hello"))
print(process_data("world"))
print(f"Total calls: {process_data.count}")
Enter fullscreen mode Exit fullscreen mode

"See how CallCounter is a class, not a function?" Timothy asked. "How does that even work as a decorator?"

The Problem: Decorators That Need Memory

"Function decorators work fine for most cases," Margaret said. "But what if you need your decorator to track something across multiple calls? Functions don't naturally hold state between invocations."

She pointed at the structure:

Tree View:

class CallCounter
    'A decorator class to count function calls.'
    __init__(self, func)
        self.func = func
        self.count = 0
    __call__(self)
        self.count += 1
        print(f'-> Calling {self.func.__name__}. Call count: {self.count}')
        Return self.func(*args, **kwargs)

@CallCounter
process_data(data)
    'Processes the input data.'
    Return data.upper()

print(process_data('hello'))
print(process_data('world'))
print(f'Total calls: {process_data.count}')
Enter fullscreen mode Exit fullscreen mode

English View:

Class CallCounter:
  Evaluate 'A decorator class to count function calls.'.
  Function __init__(self, func):
    Set self.func to func.
    Set self.count to 0.
  Function __call__(self):
    self.count += 1.
    Evaluate print(f'-> Calling {self.func.__name__}. Call count: {self.count}').
    Return self.func(*args, **kwargs).

Decorator @CallCounter
Function process_data(data):
  Evaluate 'Processes the input data.'.
  Return data.upper().

Evaluate print(process_data('hello')).
Evaluate print(process_data('world')).
Evaluate print(f'Total calls: {process_data.count}').
Enter fullscreen mode Exit fullscreen mode

How Class Decorators Work

"Look at the structure," Margaret said. "When Python sees @CallCounter above process_data, it does the same thing as with function decorators: process_data = CallCounter(process_data)."

Timothy traced through it. "So CallCounter.__init__ receives the original process_data function and stores it in self.func?"

"Exactly. And it initializes self.count to zero. Now process_data isn't a function anymore - it's an instance of CallCounter."

"But how do we call it? We're calling process_data('hello') like it's still a function."

Margaret pointed to __call__. "The magic method __call__ makes any object callable. When you write process_data('hello'), Python sees that process_data is a CallCounter instance and calls its __call__ method."

Timothy worked through the execution:

-> Calling process_data. Call count: 1
HELLO
-> Calling process_data. Call count: 2
WORLD
Total calls: 2
Enter fullscreen mode Exit fullscreen mode

"So each time we call process_data, the __call__ method runs, increments self.count, and then calls the original function stored in self.func?"

"Precisely. The class instance persists between calls, so self.count accumulates. You can't do that easily with a function decorator."

When to Use Class-Based Decorators

"So when would I choose a class over a function for my decorator?" Timothy asked.

"Use a class-based decorator when you need to track state across multiple calls, store configuration that might change, implement multiple related decorators that share behavior, or need the decorator itself to have methods you can call later."

Timothy nodded. "The structure makes it clear - __init__ sets up the decorator, __call__ handles each invocation. The class holds everything together."

"That's the pattern," Margaret confirmed. "Class-based decorators give you an object with state, methods, and attributes. Function decorators are simpler for stateless transformations. Choose the right tool for your needs."


Analyze Python structure yourself: Download the Python Structure Viewer - a free tool that shows code structure in tree and plain English views. Works offline, no installation required.


Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.

Top comments (0)