Skip to Content
Course content

191: Class-Based Decorators

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've already spent some time with function-based decorators, and for 90% of use cases, they're perfect. But every now and then, you hit a wall where you need the decorator to maintain a more complex state than a simple closure can comfortably handle. In those cases, I usually reach for a class.

Let's say we're building a system that interacts with a third-party API. We want to track exactly how many times a specific function is called—maybe for billing purposes or to detect a loop gone rogue. I could use a global variable, but that's messy. A class feels more natural here.

The first attempt at a stateful decorator

My first instinct is to create a class that stores the count and takes the function in its constructor. Let's see what happens when I try to apply this to a mock API call:

class CallCounter:
    def __init__(self, func):
        self.func = func
        self.count = 0

    def decorate(self):
        def wrapper(*args, **kwargs):
            self.count += 1
            print(f"Call count: {self.count}")
            return self.func(*args, **kwargs)
        return wrapper

@CallCounter
def fetch_data(user_id):
    print(f"Fetching data for {user_id}...")
    return {"data": "some results"}

# Let's try calling it
fetch_data(101)

If you ran this, you'd get a TypeError: 'CallCounter' object is not callable. This is a classic "gotcha." When we use the @ syntax, Python essentially does fetch_data = CallCounter(fetch_data). Now, fetch_data is no longer a function; it's an instance of the CallCounter class. Since the class doesn't know how to "act" like a function, it crashes the moment we try to invoke it.

Making the instance callable

To fix this, I need to tell Python that an instance of CallCounter should be treated as a function. That's exactly what the __call__ magic method is for. I'll ditch the decorate helper method and move the logic directly into __call__.

class CallCounter:
    def __init__(self, func):
        self.func = func
        self.count = 0

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

@CallCounter
def fetch_data(user_id):
    print(f"Fetching data for {user_id}...")
    return {"data": "some results"}

fetch_data(101)
fetch_data(102)

Now it works. The first call prints "Call count: 1" and the second prints "Call count: 2". By using a class, self.count is persisted across calls. It's cleaner than a closure if you plan on adding more state—like a timestamp of the first call or a list of arguments used in previous invocations.

Dealing with decorator arguments

Wait, there's a catch. What if I want to give my decorator a name or a specific starting count? Like @CallCounter(label="API_HIT")? If I try that with the code above, it'll break immediately. Why? Because when you add parentheses to a decorator, Python calls the class before passing it the function. Now the __init__ is receiving "API_HIT" instead of the fetch_data function.

To handle this, I need a two-stage process: one to handle the configuration and another to handle the function wrapping. This is where class decorators get a bit meta. I'll move the function wrapping into a separate __call__ layer.

class CallCounter:
    def __init__(self, label="Default"):
        self.label = label
        self.count = 0

    def __call__(self, func):
        # This is called when the @CallCounter(label="...") is applied
        def wrapper(*args, **kwargs):
            self.count += 1
            print(f"[{self.label}] Call count: {self.count}")
            return func(*args, **kwargs)
        return wrapper

@CallCounter(label="UserAPI")
def fetch_data(user_id):
    return {"id": user_id}

fetch_data(101)
fetch_data(102)

Notice the shift: __init__ now takes the label, and __call__ now takes the function. The wrapper is where the actual execution happens. I personally find this pattern slightly more readable than nested function decorators when the configuration logic starts getting heavy. It encapsulates the "counter" state within the object instance and the "execution" logic within the wrapper.




📋 Practical Task

Build a RequestThrottler Class Decorator

You need to create a class-based decorator called RequestThrottler. Its purpose is to prevent a function from being called too rapidly.

  • The decorator should accept a limit argument (an integer) in its constructor.
  • It should keep track of how many times the decorated function has been called.
  • If the count exceeds the limit, instead of executing the original function, it should print a message: "Rate limit exceeded! Maximum of X calls allowed." and return None.
  • If the count is within the limit, it should execute the function and return its result.

Test your implementation by decorating a function and calling it more times than the specified limit.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.