Background
Decorators are one of the most powerful tools in Python, allowing you to modify the behavior of functions or methods. Sometimes, it's not enough to just "wrap" a function but to configure the decorator using parameters (arguments). These cases arise in logging, time checks, access restrictions, etc.
The Issue
Regular decorators accept only one function that needs to be wrapped. When parameters need to be passed to the decorator itself, the syntax becomes more complex, often leading to errors, especially with nested functions and passing *args/**kwargs.
The Solution
A parameterized decorator is implemented through a higher-order function: first, the outer "decorating" function is called with arguments, which creates and returns the actual decorator, and the decorator then receives the function and returns the wrapper:
def repeat(n): def decorator(func): def wrapper(*args, **kwargs): result = None for _ in range(n): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(3) def greet(name): print(f"Hello, {name}!") greet("Python") # Output: Hello, Python! (3 times)
Key Features:
Why can't a parameterized decorator be implemented the same way as a regular decorator?
When you apply @decorator, Python passes the function as an argument to the decorator. If you add parentheses (@decorator()), Python first calls the function, and only its result is interpreted as a decorator.
def deco(func): # regular decorator: @deco def deco_with_args(arg): # decorator with argument: @deco_with_args(arg)
How are decorators with arguments fundamentally different from decorators without arguments at the call level?
Decorators without arguments receive a function as input, while decorators with arguments receive parameters rather than a function and return a decorator from themselves.
How to properly use functools.wraps and why is it necessary?
functools.wraps(func) in the wrapper preserves the name, documentation string, and other metadata of the original function; otherwise, all this information will be replaced in the wrapper, which hinders debugging and introspection.
import functools def deco_with_args(arg): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper return decorator
A decorator was implemented without considering parameters or with the incorrect number of nested functions:
def log(level): def wrapper(func): # error — wrapper must be deeper print(f"Log: {level}") func() # Function is not returned as a decorator return wrapper @log("INFO") def action(): print("Work!")
Pros:
Cons:
Using functools.wraps and properly nested functions:
import functools def timer(units): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): import time start = time.time() result = func(*args, **kwargs) end = time.time() if units == 'ms': duration = (end - start) * 1000 else: duration = end - start print(f"Duration: {duration:.4f} {units}") return result return wrapper return decorator @timer('ms') def op(): sum(range(1000)) op()
Pros:
Cons: