Decorators
Decorators let you modify or extend a function's behaviour without touching its source code. They power Flask routes, Django views, caching, logging, and more.
Functions are First-Class Objects
In Python, functions are objects. They can be stored in variables, passed as arguments, and returned from other functions. Decorators rely entirely on this.
def greet(name):
return f"Hello, {name}!"
say_hi = greet # store function in variable
print(say_hi("Alice")) # Hello, Alice!
def apply(func, value): # function as argument
return func(value)
print(apply(greet, "Bob")) # Hello, Bob!
def make_greeter(word): # function returned from function
def greeter(name):
return f"{word}, {name}!"
return greeter
hello = make_greeter("Hello")
hey = make_greeter("Hey")
print(hello("Carol")) # Hello, Carol!
print(hey("Dave")) # Hey, Dave!
Output
Building a Decorator
A decorator is a function that takes a function and returns a new function wrapping the original.
import functools
def logger(func):
@functools.wraps(func) # keep original name/docstring
def wrapper(*args, **kwargs):
print(f"→ calling {func.__name__}{args}")
result = func(*args, **kwargs)
print(f"← returned {result!r}")
return result
return wrapper
# Manual application
def add(a, b): return a + b
logged_add = logger(add)
logged_add(3, 4)
print("---")
# @ syntax — exactly the same thing
@logger
def multiply(x, y):
return x * y
multiply(5, 6)
Output
@ is sugar
@logger above def multiply is exactly multiply = logger(multiply).Practical Decorators
import time, functools
# Timer decorator
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
t = time.perf_counter()
result = func(*args, **kwargs)
print(f"{func.__name__} took {time.perf_counter()-t:.4f}s")
return result
return wrapper
# Decorator with argument — needs an extra layer
def repeat(n):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@timer
def slow_sum(n):
return sum(range(n))
@repeat(3)
def say(msg):
print(msg)
slow_sum(1_000_000)
say("echo!")
# Simple cache (memoisation)
def memoize(func):
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fib(n):
if n <= 1: return n
return fib(n-1) + fib(n-2)
print([fib(i) for i in range(10)])
Output