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
189: Decorators with Arguments
Up until now, you've been using decorators like @timer or @authenticated. Those are straightforward: they take the function they're decorating and return a wrapped version of it. But what happens when you want to customize the decorator's behavior? What if you want to say, "Run this function three times," or "Only allow this function to run if the user has 'Admin' level permissions"?
Wait, why do I suddenly need three levels of nested functions?
This is the part that usually trips people up. When you write @decorator, Python calls that function immediately and passes the decorated function into it. But when you write @decorator(arg), you're actually calling a function before the decoration process even starts.
Basically, you aren't writing a decorator anymore; you're writing a decorator factory. You're creating a function that returns the actual decorator. Think of it like this: the outer function handles your configuration arguments, the middle function handles the function being decorated, and the inner function handles the actual logic and the arguments passed to the original function. It feels like Inception, but it's the only way to get those custom arguments into the wrapper.
How do I actually implement this in code?
Let's look at a real-world scenario. Imagine you're writing a script that calls a flaky external API. You want a @retry decorator where you can specify how many times the code should try again before finally giving up.
import time
def retry(times):
# This is the factory. It takes the 'times' argument.
def decorator(func):
# This is the actual decorator. It takes the function.
def wrapper(*args, **kwargs):
# This is where the logic happens.
attempts = 0
while attempts < times:
try:
return func(*args, **kwargs)
except Exception as e:
attempts += 1
print(f"Attempt {attempts}/{times} failed. Retrying...")
time.sleep(1)
return func(*args, **kwargs) # Final attempt that can raise the exception
return wrapper
return decorator
@retry(times=3)
def unstable_api_call():
print("Calling API...")
# Simulating a failure
raise ConnectionError("Server is down!")
# unstable_api_call()
If you look closely, retry(3) is executed first. It returns the decorator function, which Python then uses to wrap unstable_api_call. If I decided later that a specific function needed 10 retries instead of 3, I don't have to write a new decorator; I just change the argument.
Does this break the arguments of the original function?
Nope, as long as you use *args and **kwargs in that innermost wrapper. The arguments you pass to the decorator (like times=3) are captured by the outer closure. The arguments you pass to the decorated function (like a user ID or a filename) are handled by the wrapper.
I've seen some developers try to name their arguments specifically in the wrapper to "be clear," but don't do that. It kills the flexibility of the decorator. Keep the wrapper generic so you can drop @retry onto any function in your project, regardless of whether that function takes zero arguments or twenty.
📋 Practical Task
Build a Request-Throttling Decorator
In high-performance systems, you often need to ensure a function isn't called too rapidly to avoid overwhelming a database or hitting an API rate limit. Your task is to create a decorator factory called @throttle(seconds).
Requirements:
- The decorator should take one argument:
seconds(a float or int). - The wrapper should keep track of when the decorated function was last called.
- If the function is called again before the specified
secondshave elapsed, the wrapper should print:"Throttled: Please wait X more seconds."and returnNoneinstead of executing the function. - If enough time has passed, it should execute the function normally.
Test your implementation with this:
@throttle(seconds=2)
def send_notification(message):
print(f"Notification sent: {message}")
send_notification("Hello!") # Should succeed
send_notification("Wait!") # Should be throttled
# Wait 2 seconds...
send_notification("Now!") # Should succeed
There are no comments for now.