Decorators, Callables, functools & Function Wrapping
A Decorator is a higher-order function or callable object that takes a function as an argument, extends or modifies its behavior, and returns a new callable. Mastering decorators requires understanding syntactic sugar compilation, function metadata preservation via functools.wraps, parameterized decorators, class-based decorators, and functools utilities (@lru_cache, functools.partial).
This chapter details decorator AST syntax compilation, @functools.wraps introspective magic, parameterized 3-tier decorator factories, and functools helpers.
1. Decorator Syntactic Sugar Compilation
The @decorator syntax is syntactic sugar evaluated at module load time when the def statement is compiled:
@my_decorator
def target_func(x):
return x * 2
# Compiles directly into:
def target_func(x):
return x * 2
target_func = my_decorator(target_func) # Re-binds name to wrapper function!2. Preserving Function Metadata (functools.wraps)
When a decorator wraps a target function in a inner wrapper function, the target function’s metadata (__name__, __doc__, __annotations__, __module__) is overwritten by the wrapper function!
# ❌ TRAP: Omitting @functools.wraps destroys function metadata!
def bad_logger(func):
def wrapper(*args, **kwargs):
print("Calling", func.__name__)
return func(*args, **kwargs)
return wrapper
@bad_logger
def add(a: int, b: int) -> int:
"""Adds two numbers."""
return a + b
print(add.__name__) # Prints "wrapper" (NOT "add"!)
print(add.__doc__) # Prints None (Docstring LOST!)Production Fix:
Always decorate wrapper functions with @functools.wraps(func):
from functools import wraps
def production_logger(func):
@wraps(func) # Preserves __name__, __doc__, __annotations__, and sets __wrapped__!
def wrapper(*args, **kwargs):
print(f"Executing {func.__name__}")
return func(*args, **kwargs)
return wrapper@wraps also sets wrapper.__wrapped__ = func, allowing introspective access to the original un-decorated function for unit testing!
3. Parameterized Decorators (3-Tier Decorator Factories)
To accept arguments inside a decorator (@retry(max_attempts=3, backoff=2)), add an outer factory function layer that returns the actual decorator:
from functools import wraps
import time
def retry(max_attempts: int = 3, backoff: float = 1.0):
# Tier 1: Factory accepting decorator parameters
def decorator(func):
# Tier 2: Actual Decorator accepting target function
@wraps(func)
def wrapper(*args, **kwargs):
# Tier 3: Inner Wrapper executing call
attempts = 0
while attempts < max_attempts:
try:
return func(*args, **kwargs)
except Exception as e:
attempts += 1
if attempts == max_attempts:
raise
time.sleep(backoff * (2 ** (attempts - 1)))
return wrapper
return decorator4. functools Utilities: @lru_cache & partial
@functools.lru_cache(maxsize=128): Memoizes function call results using a least-recently-used (LRU) hash map. Arguments passed to cached functions must be hashable.functools.partial(func, *args, **kwargs): Freezes a portion of a function’s positional and keyword arguments, returning a new callable with a reduced signature.