A decorator is a function that wraps another function, adding behavior before, after, or around its execution — Python's idiomatic mechanism for reusable cross-cutting logic like logging, timing, or caching.
import functools
import time
def timer(func):
@functools.wraps(func) # preserves func's name/docstring on the wrapper
def wrapper(*args, **kwargs): # accepts ANY arguments, forwards them untouched
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} took {time.time() - start:.4f}s")
return result
return wrapper
@timer # equivalent to: slow_function = timer(slow_function)
def slow_function():
time.sleep(1)
return "done"
slow_function() # prints timing info, then returns "done"
from functools import lru_cache
@lru_cache(maxsize=None) # caches results — repeated calls with the same args are instant
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
@decorator above a function definition is exactly equivalent to writing func = decorator(func) immediately after defining it — the @ syntax is purely a readability convenience. The wrapper function typically accepts *args, **kwargs so it can wrap any function's signature without knowing its specific parameters in advance, then forwards them unchanged to the original function.
@functools.wraps(func) is a small but important detail: without it, a decorated function's __name__ and docstring would be replaced by the wrapper's, breaking introspection tools, help(), and debugging — functools.wraps copies that metadata over automatically.
functools.lru_cache is a genuinely practical, ready-to-use decorator from the standard library — it memoizes a function's results, so calling fibonacci(30) a second time with the same argument returns instantly instead of recomputing, turning the classic exponential-time naive recursive Fibonacci into a fast, cached version with a single decorator line.