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 decorator

4. 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.
Display Options
Appearance
Text Size
100%