Decorators
A decorator is a function that takes another function, adds behavior around it, and returns a (usually wrapped) function. Decorators rely on functions being first-class objects and on closures.
The Manual Version First (What @ Actually Does)
def shout(text):
return text.upper()
def loud(func):
def wrapper(text):
return func(text) + "!!!"
return wrapper
shout = loud(shout) # manually re-assign shout to the wrapped version
shout("hi") # 'HI!!!'The @ Syntax (Syntactic Sugar for the Above)
def loud(func):
def wrapper(text):
return func(text) + "!!!"
return wrapper
@loud
def shout(text):
return text.upper()
shout("hi") # 'HI!!!' -- identical result to the manual versionDecorators That Work with Any Function Signature
Using *args, **kwargs (see Args-and-Kwargs) lets a decorator wrap functions regardless of their parameters.
import functools
import time
def timer(func):
@functools.wraps(func) # preserves func.__name__, __doc__, etc
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timer
def slow_function(n):
return sum(range(n))
slow_function(1_000_000)Always use
@functools.wraps(func)Without it,
wrapper.__name__becomes'wrapper'instead of the original function’s name, which breaks introspection, debugging, and documentation tools. This is easy to forget and hard to notice until something downstream (likehelp()or a framework’s routing table) misbehaves.
Decorators with Arguments
A decorator factory: a function that returns a decorator, allowing the decorator itself to be configured.
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(3)
def greet():
print("Hello")
greet() # prints 'Hello' 3 timesThree levels deep
repeat(3)-> returnsdecorator-> which wrapsgreet-> and returnswrapper. This three-layer nesting (factory -> decorator -> wrapper) is the standard shape for any parameterized decorator.
Chaining Multiple Decorators
Decorators apply bottom-up (closest to the function first).
@timer
@repeat(2)
def task():
print("working")
# equivalent to: task = timer(repeat(2)(task))Common Built-in Decorators
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def area(self): # access like an attribute: circle.area
return 3.14159 * self._radius ** 2
@staticmethod
def unit_circle(): # no self/cls, just a namespaced function
return Circle(1)
@classmethod
def from_diameter(cls, diameter): # receives the class, not an instance
return cls(diameter / 2)See Class-and-Static-Methods and Encapsulation-and-Properties for these in depth.
functools.lru_cache: Memoization Decorator
Caches function results, dramatically speeding up expensive or recursive calls with repeated inputs.
from functools import lru_cache
@lru_cache(maxsize=None)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
fibonacci(35) # fast, without caching this would take a very long time
lru_cacherequires hashable argumentsFunctions decorated with
lru_cachemust be called with hashable arguments (numbers, strings, tuples), not lists or dicts, since the cache uses the arguments as dictionary keys internally.
Class-Based Decorators
Any object with a __call__ method can act as a decorator.
class CountCalls:
def __init__(self, func):
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"Call #{self.count} to {self.func.__name__}")
return self.func(*args, **kwargs)
@CountCalls
def say_hi():
print("hi")
say_hi() # Call #1 to say_hi / hi
say_hi() # Call #2 to say_hi / hiSee Magic-Dunder-Methods for more on __call__.