The earlier OOP lesson used @property without explaining the mechanism behind it. A decorator is a function that takes another function, wraps it with extra behavior, and returns the wrapped version — the @ syntax is just a convenient way to apply one.
def shout(func):
def wrapper():
result = func()
return result.upper()
return wrapper
def greet():
return 'hello'
loud_greet = shout(greet)
print(loud_greet()) # HELLOdef shout(func):
def wrapper():
result = func()
return result.upper()
return wrapper
@shout
def greet():
return 'hello'
print(greet()) # HELLO — @shout applied automatically@shout above def greet(): is exactly equivalent to writing greet = shout(greet) right after defining it.
A real decorator needs to pass through whatever arguments the wrapped function actually takes, without knowing them in advance.
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f'{func.__name__} took {time.time() - start:.4f}s')
return result
return wrapper
@timer
def slow_add(a, b):
time.sleep(1)
return a + b
slow_add(2, 3) # prints: slow_add took 1.0003s| Decorator | What it does |
|---|---|
| @property | Lets a method be accessed like an attribute — covered in the earlier OOP lesson |
| @staticmethod | Marks a method that doesn't use self at all |
| @classmethod | Passes the class itself (cls) instead of an instance |
| @functools.lru_cache | Caches a function's return value for repeated calls with the same arguments |
Why this pattern is everywhere in real code
A decorator is one of the cleanest ways to add cross-cutting behavior — logging, timing, caching, access checks — without repeating that logic inside every function it applies to.