MA-PY1 · Module 4.6
Decorators and context managers
Topics: decorator · functools.wraps · with · __enter__ · __exit__
Overview
Module 4.6 — Decorators and context managers
Scroll Core ideas → worked snippets → Practice (quiz). Attempt each question before Discuss / reveal. Use any Python REPL, script, or notebook you like.
Concepts
Core ideas — decorators & context managers
Decorators (callables transforming callables)
A decorator D takes a function f and returns a new callable f'—usually wrapping f to add timing, logging, validation, retries, etc.
def loud(f):
def wrapper(*args, **kwargs):
print(f"calling {f.__name__}")
return f(*args, **kwargs)
return wrapper
@loud # countdown = loud(countdown)
def countdown(n):
return list(range(n, -1, -1))
Writing **@D directly above def is equivalent to assigning fn = D(fn) after def finishes.
Stacked decorators apply bottom-up:
@a
@b
def f(): ...
means f = a(b(f)) (the decorator **immediately above def applies first: here b wraps f first, then a wraps that result).
Preserve help() names/docs on the wrapper using functools.wraps:
import functools
def traced(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
print("enter")
try:
return f(*args, **kwargs)
finally:
print("leave")
return wrapper
Parameterized decorators (“decorator factories”) accept configuration and return the real decorator—you see two nested def layers (retry(n) → deco(f) → wrapper).
Standard-library patterns you’ll read often: @classmethod, @staticmethod, @property (descriptor-based, decorator-shaped). Earlier, with classes, these were deferred; now you can see why they exist — they are ordinary decorators that adjust how a method is called. @staticmethod drops the automatic self; @classmethod swaps self for the class itself (handy for alternative constructors); @property lets a method be read like a plain attribute. You do not need to write your own yet, but you will now recognise the shape when you read them.
Context managers (with)
with guarantees cleanup runs when leaving the block—even on exceptions—when the object implements __enter__ and __exit__ (or you use contextlib.contextmanager with a yield generator).
with open(path, encoding="utf-8") as f:
data = f.read()
# file closed here
path in snippets should be replaced with a real string or Path in your sandbox.
Contrast: decorators shape callable objects for reuse; context managers orchestrate lifetimes. They often appear together in frameworks.
Self-study checklist
- Confirm stacking order
@athen@bwithprintstubs printing which wrapper runs outermost first call. - Read **
functools.wrapsdocumentation once—note which metadata propagate. - List three chores
withexcels at (files, locks, timing sections withcontextlib, DB transactions…).
Examples
Worked snippets — decorators & with
Basic decorator
import functools
def starred(f):
@functools.wraps(f)
def wrapper(name):
return "-- " + f(name) + " --"
return wrapper
@starred
def salute(name):
return f"hi {name}"
print(salute.__name__)
print(salute("Ada"))
Timing wrapper (minimal)
import functools
import time
def timed(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
t0 = time.perf_counter()
try:
return f(*args, **kwargs)
finally:
print(f"{f.__name__}: {time.perf_counter() - t0:.6f}s")
return wrapper
@timed
def slow_sum(n):
return sum(range(n))
print(slow_sum(10_000))
Decorator stacking order
import functools
def annotate(tag):
def deco(f):
@functools.wraps(f)
def wrapper():
print(tag, end=" ")
return f()
return wrapper
return deco
@annotate("A")
@annotate("B")
def speak():
print("speech")
speak() # outermost annotate runs immediately around inner wrappers
(Expect A B before speech with this construction—still verify locally.)
classmethod refresher
class Thing:
kind = "widget"
@classmethod
def label(cls):
return cls.kind
print(Thing.label())
Simple context manager via class
class Indent:
def __init__(self, txt):
self.txt = txt
def __enter__(self):
print(self.txt + " start")
return self
def __exit__(self, exc_type, exc, tb):
print(self.txt + " end")
return False
with Indent("phase"):
print(" body")
with invokes __enter__, runs the body indented block, then __exit__ even if body raises.
Practice check — decorators and context managers
Multiple-choice check from the Matheion app lesson. Pick an answer for each question, then submit.
1.Writing `@deco` directly above `def f(): ...` is equivalent to which statement after the definition?
2.Given stacked decorators, what is the effective call order? ```python @a @b def f(): ... ```
3.Why wrap a decorator's inner function with `@functools.wraps(f)`?
4.What does a `with` block guarantee for a proper context manager, even if an exception occurs inside it?
5.Which pair of methods must an object implement to be usable as a context manager in a `with` statement?