Kodokon kodokon.com

Decorators and higher-order functions

Leverage functions as first-class objects to write robust, parameterizable and transparent decorators.

10 min · 3 questions

Open this lesson in Kodokon

In Python, a function is an object like any other: you can assign it to a variable, pass it as an argument, return it. A higher-order function takes or returns a function - sorted(data, key=...) is one. Add closures, where an inner function captures the variables of the enclosing function and keeps them alive after it returns, and you have all the ingredients of decorators.

PYTHON
def make_multiplier(factor):
    def multiply(value):
        return value * factor
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)
print(double(10), triple(10))  # 20 30
A closure: multiply captures factor

A decorator is a function that receives a function and returns another one - usually a wrapper that adds behavior around the call. The syntax @timed above def slow_sum is strict sugar for slow_sum = timed(slow_sum). The wrapper accepts *args, kwargs to stay agnostic about the signature, and returns** the result of the decorated function - forgetting this is a classic mistake that turns every return value into None.

PYTHON
import functools
import time

def timed(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        delay = time.perf_counter() - start
        print(f"{func.__name__}: {delay:.4f}s")
        return result
    return wrapper

@timed
def slow_sum(n):
    return sum(range(n))

print(slow_sum(1_000_000))
A timing decorator, production-ready

To parameterize a decorator (@repeat(times=3)), you need one more level: a factory that receives the parameters and returns the decorator, which in turn returns the wrapper. Three nested functions - that is the price of flexibility. If the state gets complex, a class with a __call__ method is often more readable. Reserve decorators for cross-cutting concerns: logging, caching, retry, access control. A decorator that alters business logic hides the essential from the reader.

PYTHON
import functools

def repeat(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def greet(name):
    print(f"Hello {name}")

greet("Ada")
Parameterized decorator: factory, decorator, wrapper

Knowledge check

Make sure you remember the key points of this lesson.

  1. What is the syntax @timed placed above def slow_sum equivalent to?
    • timed(slow_sum())
    • slow_sum = timed(slow_sum)
    • slow_sum.timed = True
  2. What problem does @functools.wraps solve?
    • It speeds up the call to the decorated function
    • It preserves the name and docstring of the decorated function
    • It makes the decorator compatible with class methods
  3. In @repeat(times=3), what does the call repeat(times=3) return?
    • The result of the decorated function
    • A decorator, ready to receive the function
    • The final wrapper directly
    • A list of three results