Closures and decorators
Capture configuration and wrap function calls
Closures
Functions are objects that can be passed, stored, and returned. A nested function can retain access to enclosing bindings after the outer call finishes; those bindings form a closure.
Closures suit a small operation with configuration. Use nonlocal to rebind an enclosing name; reading or mutating an object already referenced there does not require it. A class may be clearer when several operations share substantial state.
Decorators
A decorator receives a function and returns its replacement. @decorate binds the defined name to decorate(function). A transparent wrapper forwards arguments, results, and exceptions.
from functools import wraps
events = []
def traced(function):
@wraps(function)
def wrapper(*args, **kwargs):
events.append("enter")
try:
return function(*args, **kwargs)
finally:
events.append("exit")
return wrapper
@traced
def quotient(left, right):
return left / right
print(quotient(8, 2), quotient.__name__) # 4.0 quotient
print(events) # ['enter', 'exit']finally records exit on both success and failure. The observer must avoid masking the original exception with its own failure. functools.wraps preserves metadata and exposes __wrapped__; it does not enforce runtime types or return contracts.
Decorators run when the function is defined. Stacking them composes replacements from the inside out; order matters for authorization, retries, and metrics. Retries can repeat side effects, and caching can share a mutable result across callers.
Gotchas
A closure retains bindings, not frozen values. Loop callbacks can therefore share the final loop value:
late = [lambda: index for index in range(3)]
captured = [lambda index=index: index for index in range(3)]
print([callback() for callback in late]) # [2, 2, 2]
print([callback() for callback in captured]) # [0, 1, 2]The default argument is evaluated when each lambda is created. It still captures an object reference, not a deep snapshot. Use a named factory when the binding logic needs more than a short expression.
Closure state persists while the returned function is reachable; concurrent mutation needs synchronization. A synchronous wrapper around async def observes coroutine creation rather than execution unless an async wrapper awaits it. Generator wrappers similarly need to choose between creation and consumption.
Put it into practice
Implement the exercise in a full-screen workspace, then run its tests.