A decorator is a function that takes a function and returns a (usually wrapped) function — @decorator above a function definition is exactly equivalent to reassigning the function to the decorator's return value:
import functools
import time
def timer(func):
@functools.wraps(func) # preserves func's __name__, docstring, etc. on the wrapper
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_add(a, b):
time.sleep(0.1)
return a + b
slow_add(2, 3)
# "slow_add took 0.1003s"
# 5
@timer above def slow_add is syntactic sugar for slow_add = timer(slow_add). The *args, **kwargs in wrapper is what lets one decorator transparently work on functions with any signature, without knowing their parameters ahead of time. functools.wraps matters more than it looks — without it, slow_add.__name__ would report "wrapper" instead of "slow_add", breaking introspection, debugging output, and tools that rely on function metadata.
A decorator factory is a function that returns a decorator, adding one more layer of nesting:
def retry(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(times):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Attempt {attempt + 1} failed: {e}")
raise
return wrapper
return decorator
@retry(times=3)
def flaky_call():
...
@retry(times=3) first calls retry(3), which returns decorator; decorator is then applied to flaky_call exactly like a normal decorator. This is why @retry(times=3) needs the parentheses but @timer above doesn't — retry is a factory that must be called to produce the actual decorator.
@property, @staticmethod, @classmethodclass Circle:
def __init__(self, radius):
self._radius = radius
@property
def area(self):
return 3.14159 * self._radius ** 2 # accessed like an attribute, not a method call
@staticmethod
def from_diameter(diameter):
return Circle(diameter / 2) # doesn't need self or cls at all
@classmethod
def unit_circle(cls):
return cls(radius=1) # cls lets subclasses override the returned type
c = Circle(2)
c.area # 12.56636 — no parentheses, looks like a plain attribute
Circle.from_diameter(4) # Circle(radius=2.0)
Circle.unit_circle() # Circle(radius=1)
@property is what makes computed attributes possible without changing the calling code if a plain attribute later needs to become a computed one. @staticmethod is essentially a plain function namespaced inside a class for organization; @classmethod receives the class itself (cls) rather than an instance, which matters for alternative constructors that need to work correctly with subclasses.