Level: Advanced | Prerequisite: Week 1
Learning Goals
- Define and apply function decorators
- Create decorators that accept arguments
- Implement context managers based on Class/Function
- Understand the role of
@functools.wraps
1. What is a Decorator
Takes a function as an argument and returns a new function. Adds functionality cross-cuttingly (logging, caching, authentication, etc.).
def loud(func):
def wrapper(*args, **kwargs):
print(f">>> {func.__name__} call")
result = func(*args, **kwargs)
print(f"<<< Result: {result}")
return result
return wrapper
@loud
def add(a, b):
return a + b
add(3, 5)
#>>> add call
#<<< Result: 8
@loud is syntactic sugar for add = loud(add).
2. functools.wraps
Even when wrapped with a decorator, the original function's name and docstring are preserved.
from functools import wraps
def loud(func):
@wraps(func) #Without this, .__name__ becomes 'wrapper'
def wrapper(*args, **kwargs):
...
return wrapper
3. Decorators that Take Arguments
A function that creates decorators (triple-nested function).
def retry(times):
def deco(func):
@wraps(func)
def wrapper(*args, **kwargs):
for i in range(times):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Retry {i+1}/{times}: {e}")
raise
return wrapper
return deco
@retry(3)
def fragile():
...
4. Context Managers
Works with the with statement. Safely acquires and releases resources.
Class-based
class FileLock:
def __init__(self, path):
self.path = path
def __enter__(self):
print(f"Lock: {self.path}")
return self
def __exit__(self, exc_type, exc, tb):
print("Unlock")
with FileLock("data.txt"):
print("In progress")
Function-based (contextmanager)
from contextlib import contextmanager
import time
@contextmanager
def timer(label):
start = time.perf_counter()
yield #Executed with block here
print(f"{label}: {time.perf_counter() - start:.4f}s")
with timer("Task"):
sum(range(1_000_000))
5. Practical — Memoization
def memoize(func):
cache = {}
@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 < 2: return n
return fib(n-1) + fib(n-2)
fib(100) #Fast
Key Examples (examples/)
| File | Content |
|---|---|
01_basic_decorator.py |
Function Decorator + wraps |
02_param_decorator.py |
Decorator that takes arguments (retry) |
03_context_class.py |
Class-based Context Manager |
04_contextmanager.py |
@contextmanager Function-based |
⚠️ Common Mistakes
- Missing
@wraps— Debugging and documentation are broken. Always include it. - Decorator doesn't have
return— Result is None. - More than one yield in
@contextmanagerfunction — Only once. - Ignoring exceptions in context manager — Swallow exceptions if
__exit__returns True.
❓ FAQ
Q1. When do you use decorators? A. When adding the same additional behavior (logging, authentication, caching) to multiple functions.
Q2. What is the difference between with and try-finally?
A. with is a standardized try-finally. Improves readability and prevents mistakes.
Q3. What happens if you stack multiple decorators?
A. The one below is applied first. @A @B def f() → f = A(B(f)).
📝 Exercises (exercises/)
exercise1.md—@loggedfunction call loggingexercise2.md—@cachedmemoizationexercise3.md—with timer("...")context manager
Next Week
Week 3. Deep Dive into Iterators & Generators