Skip to Content
Course content

189: Decorators with Arguments

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

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 seconds have elapsed, the wrapper should print: "Throttled: Please wait X more seconds." and return None instead 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
Rating
0 0

There are no comments for now.

to be the first to leave a comment.