7 min read
ā¢Question 25 of 41mediumClosures in Python
Understanding closures and scope.
What You'll Learn
- What closures are and how they work
- Python's LEGB scope resolution
- The nonlocal keyword
- Common closure patterns
- Closures vs classes
What is a Closure?
A closure is a function that "remembers" variables from its enclosing scope, even after that scope has finished executing. It's created when a nested function references variables from its outer function.
code.pyPython
def outer(x):
# x is in the enclosing scope
def inner(y):
return x + y # x is "closed over"
return inner # Return the function object
# Create closures with different x values
add_5 = outer(5)
add_10 = outer(10)
print(add_5(3)) # 8 (5 + 3)
print(add_10(3)) # 13 (10 + 3)
# The functions "remember" their x values
print(add_5(7)) # 12 (5 + 7)Python's LEGB Scope Rule
Python searches for variables in this order:
code.pyPython
# LEGB: Local -> Enclosing -> Global -> Built-in
x = "global" # Global scope
def outer():
x = "enclosing" # Enclosing scope
def inner():
x = "local" # Local scope
print(x) # "local"
inner()
print(x) # "enclosing"
outer()
print(x) # "global"The nonlocal Keyword
To modify a variable in the enclosing scope (not just read it), use nonlocal:
code.pyPython
def counter():
count = 0 # Enclosing variable
def increment():
nonlocal count # Declare intent to modify
count += 1
return count
return increment
c = counter()
print(c()) # 1
print(c()) # 2
print(c()) # 3
# Without nonlocal, this would create a local variable
def broken_counter():
count = 0
def increment():
count += 1 # UnboundLocalError!
return count
return incrementPractical Closure Patterns
Factory Functions
code.pyPython
def make_multiplier(n):
"""Create a function that multiplies by n."""
def multiplier(x):
return x * n
return multiplier
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
# Power functions
def make_power(exp):
def power(base):
return base ** exp
return power
square = make_power(2)
cube = make_power(3)Stateful Functions
code.pyPython
def running_average():
"""Maintain running average without a class."""
total = 0
count = 0
def average(value):
nonlocal total, count
total += value
count += 1
return total / count
return average
avg = running_average()
print(avg(10)) # 10.0
print(avg(20)) # 15.0
print(avg(30)) # 20.0Configurable Functions
code.pyPython
def logger(prefix):
"""Create a logger with a fixed prefix."""
def log(message):
print(f"[{prefix}] {message}")
return log
info = logger("INFO")
error = logger("ERROR")
info("Server started") # [INFO] Server started
error("Connection failed") # [ERROR] Connection failedMemoization
code.pyPython
def memoize(func):
"""Cache function results using closure."""
cache = {}
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(100)) # Instant (cached)Closures vs Classes
| Aspect | Closure | Class |
|---|---|---|
| Syntax | Function-based | Class-based |
| State | In enclosing scope | In instance attributes |
| Methods | Usually one | Multiple methods |
| Visibility | Truly private | Convention (_var) |
| Use Case | Simple stateful functions | Complex objects |
code.pyPython
# Closure approach
def make_account(initial_balance):
balance = initial_balance
def transact(amount):
nonlocal balance
balance += amount
return balance
return transact
# Class approach
class Account:
def __init__(self, initial_balance):
self.balance = initial_balance
def transact(self, amount):
self.balance += amount
return self.balance
# Usage is similar
closure_account = make_account(100)
class_account = Account(100)
closure_account(-20) # 80
class_account.transact(-20) # 80Common Pitfall: Late Binding
code.pyPython
# Problem: All functions share the same i!
funcs = []
for i in range(3):
funcs.append(lambda: i)
print([f() for f in funcs]) # [2, 2, 2] - Not [0, 1, 2]!
# Solution: Capture value with default argument
funcs = []
for i in range(3):
funcs.append(lambda i=i: i)
print([f() for f in funcs]) # [0, 1, 2]Interview Tip
When asked about closures:
- A closure captures variables from enclosing scope
- LEGB: Local ā Enclosing ā Global ā Built-in
- Use nonlocal to modify enclosing variables
- Common for factories, decorators, callbacks
- Watch for late binding in loops