Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
191: Class-Based Decorators
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
limitargument (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 returnNone. - 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.
There are no comments for now.