Functions that remember: closures
A function defined inside another function remembers the outer function's variables, even after the outer function has returned. That lets you configure a function once, or give it private state.
- Write a factory: an outer function that defines and returns an inner function
- Explain why each returned function keeps its own copy of the outer variables
- Update remembered state with
nonlocal, and recognise the UnboundLocalError when it is missing
Functions are values: you have passed them as keys. A function can also return a function. An inner def can use the outer function's parameters, and Python keeps those variables alive for as long as the inner function exists. That combination of a function and the variables it remembers is a closure.
The use: configure once, call many times. A shop has a member discount and a clearance discount. Instead of passing the rate to every call, build one function per discount.
Run each example to see what it prints. Running examples doesn't count toward progress.
def make_discount(rate): def apply(price): return price * (1 - rate) return apply member = make_discount(0.10) clearance = make_discount(0.50) print(member(40), clearance(40)) print(member(25.0))
A closure can also keep state that changes between calls, such as a counter. Reading an outer variable works directly, but assigning to it (count += 1) would make Python treat count as a new local variable of the inner function. nonlocal count says: this name is the outer function's variable; update that one.
def make_numberer(prefix): count = 0 def next_number(): nonlocal count count += 1 return f"{prefix}-{count:04d}" return next_number invoice_no = make_numberer("INV") refund_no = make_numberer("RF") print(invoice_no(), invoice_no(), refund_no(), invoice_no())
def make_counter(): count = 0 def increment(): count += 1 return count return increment clicks = make_counter() print(clicks())
When to use it: a configured helper (a discount, a formatter, a validator with its limits) or a small private state (an ID generator, a rate limiter) without needing a class. Mistakes to avoid: forgetting nonlocal when updating; returning apply() (the result of calling it) instead of apply (the function); and expecting two closures from the same factory to share state: each call to the factory makes fresh variables.
- An inner function can use the outer function's variables; returned, it keeps them: a closure.
- Each call to the factory creates fresh variables, so each returned function has its own.
- Assigning to a remembered variable needs
nonlocal, or Python raises UnboundLocalError. - Next: a function that calls itself, to walk data nested to any depth.