Timothy had mastered basic decorators, but the head librarian's next request revealed a limitation. "I need different logging levels for different functions—some should log detailed information, others just errors. Can your decorators handle configuration?"
Margaret led him deeper into the Function Modification Station, to a section labeled "The Parameter Laboratory" where decorators themselves could accept arguments.
The Configuration Problem
Timothy's basic logging decorator was inflexible:
@add_logging
def catalog_book(title):
return f"Cataloged: {title}"
Every function got the same logging behavior. But what if Timothy wanted to specify the logging level?
@add_logging(level="DEBUG")
def catalog_book(title):
return f"Cataloged: {title}"
@add_logging(level="ERROR")
def delete_book(book_id):
return f"Deleted: {book_id}"
Margaret explained this required an extra layer of function nesting.
The Triple-Layer Pattern
"When a decorator needs arguments," Margaret said, "you need three layers instead of two."
def add_logging(level="INFO"):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"[{level}] Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"[{level}] Completed {func.__name__}")
return result
return wrapper
return decorator
Timothy traced the logic:
-
Outer function (
add_logging
) accepts configuration parameters and returns a decorator -
Middle function (
decorator
) accepts the function to be decorated and returns a wrapper -
Inner function (
wrapper
) executes the actual enhanced behavior
When Python encountered @add_logging(level="DEBUG")
, it first called add_logging("DEBUG")
which returned the actual decorator function, then applied that decorator to the target function.
The Execution Flow
Timothy visualized how the layers unwrapped:
@add_logging(level="DEBUG")
def search_catalog(query):
return f"Searching: {query}"
# Equivalent to:
# temp_decorator = add_logging(level="DEBUG")
# search_catalog = temp_decorator(search_catalog)
The parentheses after @add_logging
triggered a function call that produced the actual decorator, which then wrapped the function below it.
The Retry Pattern
Margaret showed Timothy a practical example: a retry decorator that attempted failed operations multiple times.
def retry(attempts=3, delay=1):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(attempts):
try:
return func(*args, **kwargs)
except Exception as error:
if attempt == attempts - 1:
raise
print(f"Attempt {attempt + 1} failed, retrying...")
time.sleep(delay)
return wrapper
return decorator
@retry(attempts=3, delay=2)
def fetch_remote_catalog():
# Might fail due to network issues
return fetch_data_from_server()
The configuration parameters (attempts
and delay
) controlled the retry behavior without changing the decorator's core logic.
The Optional Arguments Pattern
Timothy discovered a challenge: what if arguments should be optional?
@add_logging # No parentheses - should use defaults
def some_function():
pass
@add_logging(level="DEBUG") # With arguments
def other_function():
pass
Margaret showed him the solution using a conditional check:
def add_logging(func=None, *, level="INFO"):
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
print(f"[{level}] {f.__name__}")
return f(*args, **kwargs)
return wrapper
if func is None:
return decorator
else:
return decorator(func)
The *
forced level
to be keyword-only, and checking whether func
was provided determined whether arguments were used.
The Validation Decorator
Timothy created a decorator that validated function arguments:
def validate_range(min_value=0, max_value=100):
def decorator(func):
@wraps(func)
def wrapper(value):
if not min_value <= value <= max_value:
raise ValueError(
f"Value {value} outside range [{min_value}, {max_value}]"
)
return func(value)
return wrapper
return decorator
@validate_range(min_value=1, max_value=999)
def set_book_count(count):
return f"Setting count to {count}"
The decorator's parameters configured the validation boundaries without cluttering the function logic.
The Rate Limiting Example
Margaret demonstrated a sophisticated pattern: rate limiting function calls.
def rate_limit(max_calls=10, time_window=60):
def decorator(func):
calls = []
@wraps(func)
def wrapper(*args, **kwargs):
now = time.time()
calls[:] = [call_time for call_time in calls
if now - call_time < time_window]
if len(calls) >= max_calls:
raise Exception(
f"Rate limit exceeded: {max_calls} calls per {time_window}s"
)
calls.append(now)
return func(*args, **kwargs)
return wrapper
return decorator
@rate_limit(max_calls=5, time_window=60)
def search_external_database(query):
return perform_search(query)
The decorator maintained state (the calls
list) in its closure, using the parameters to configure the rate limiting thresholds.
The Class-Based Alternative
Timothy learned that decorators could also be implemented as classes:
class CountCalls:
def __init__(self, func):
self.func = func
self.call_count = 0
def __call__(self, *args, **kwargs):
self.call_count += 1
print(f"Call {self.call_count} to {self.func.__name__}")
return self.func(*args, **kwargs)
@CountCalls
def process_book(title):
return f"Processing: {title}"
The __init__
method received the function, and __call__
made instances callable, executing the wrapped behavior. For decorators with parameters, the pattern extended:
class LogLevel:
def __init__(self, level="INFO"):
self.level = level
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"[{self.level}] {func.__name__}")
return func(*args, **kwargs)
return wrapper
@LogLevel(level="DEBUG")
def catalog_item(item):
return f"Cataloged: {item}"
The Decorator Factory Pattern
Margaret emphasized the mental model: decorators with arguments were decorator factories.
# Without arguments - this IS the decorator
@simple_decorator
def func():
pass
# With arguments - this CREATES the decorator
@decorator_factory(arg1, arg2)
def func():
pass
The factory accepted configuration and returned a customized decorator, which then wrapped the target function.
Timothy's Advanced Decorator Wisdom
Through exploring the Parameter Laboratory, Timothy learned key principles:
Three layers for arguments: Outer function accepts parameters, middle accepts the function, inner executes the wrapper.
Factories create decorators: @decorator(args)
calls the factory to produce the actual decorator.
Closures hold configuration: Parameters from the outer function remain accessible in the wrapper.
Optional arguments need special handling: Check whether the function was provided directly or configuration was given.
Classes work too: Implement __call__
to make instances act as decorators.
State can be maintained: The decorator's closure can store data across calls.
Timothy's mastery of parameterized decorators revealed that decorators could be as configurable as needed. The three-layer pattern seemed complex at first, but it followed logically from the basic decorator pattern—just one more level of abstraction to handle configuration. The Parameter Laboratory transformed decorators from simple wrappers into sophisticated, configurable enhancement systems.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.
Top comments (0)