Closures & Scope
Closures & Scope¶
1. Variable Scope¶
In Python, variable access scope is determined by where it's defined.
LEGB Rule¶
The order in which variables are searched.
┌─────────────────────────────────────────────┐
│ B - Built-in │
│ print, len, range, ... │
│ ┌─────────────────────────────────────────┐ │
│ │ G - Global │ │
│ │ Variables defined at module level │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ E - Enclosing (enclosing function) │ │ │
│ │ │ Local variables of outer function │ │ │
│ │ │ ┌─────────────────────────────────┐ │ │ │
│ │ │ │ L - Local │ │ │ │
│ │ │ │ Inside current function │ │ │ │
│ │ │ └─────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
Example¶
# Built-in (B)
# print, len, str, ...
# Global (G)
x = "global"
def outer():
# Enclosing (E)
x = "enclosing"
def inner():
# Local (L)
x = "local"
print(x) # local
inner()
print(x) # enclosing
outer()
print(x) # global
2. global Keyword¶
Used to modify global variables inside functions.
count = 0
def increment():
global count # Declare global variable use
count += 1
increment()
increment()
print(count) # 2
Without global?¶
count = 0
def increment():
count += 1 # UnboundLocalError!
# count recognized as local variable in count = count + 1
increment()
Read-Only Doesn't Need global¶
name = "Python"
def greet():
print(f"Hello, {name}") # Can read without global
greet() # Hello, Python
3. nonlocal Keyword¶
Used to modify variables in enclosing functions.
def outer():
count = 0
def inner():
nonlocal count # Declare use of outer function's variable
count += 1
inner()
inner()
print(count) # 2
outer()
global vs nonlocal¶
x = "global"
def outer():
x = "outer"
def inner():
nonlocal x # Modify outer's x
x = "inner"
inner()
print(f"In outer: {x}") # inner
outer()
print(f"In global: {x}") # global (unchanged)
4. Closures¶
A closure is a function that remembers the environment (scope) where it was defined.
Closure Conditions¶
- Must have nested functions
- Inner function must reference outer function's variables
- Outer function must return inner function
Basic Example¶
def make_multiplier(n):
"""Return function that multiplies by n"""
def multiplier(x):
return x * n # Remembers n
return multiplier
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
print(double(10)) # 20
Check Closure Variables¶
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
c = make_counter()
print(c.__closure__) # Check closure cells
print(c.__closure__[0].cell_contents) # 0
c()
print(c.__closure__[0].cell_contents) # 1
5. Closure Usage Patterns¶
Factory Functions¶
def make_power(exp):
"""Generate power function"""
def power(base):
return base ** exp
return power
square = make_power(2)
cube = make_power(3)
print(square(4)) # 16
print(cube(4)) # 64
State Maintenance¶
def make_accumulator(initial=0):
"""Create accumulator"""
total = initial
def add(value):
nonlocal total
total += value
return total
return add
acc = make_accumulator(100)
print(acc(10)) # 110
print(acc(20)) # 130
print(acc(30)) # 160
Configuration Storage¶
def make_logger(prefix):
"""Create logger with prefix"""
def log(message):
print(f"[{prefix}] {message}")
return log
error_log = make_logger("ERROR")
info_log = make_logger("INFO")
error_log("Problem occurred!") # [ERROR] Problem occurred!
info_log("Starting") # [INFO] Starting
Function Customization¶
def make_formatter(template):
"""Create template-based formatter"""
def format_data(**kwargs):
return template.format(**kwargs)
return format_data
user_format = make_formatter("Name: {name}, Age: {age}")
product_format = make_formatter("{name} - {price} USD")
print(user_format(name="Alice", age=30))
# Name: Alice, Age: 30
print(product_format(name="Apple", price=1000))
# Apple - 1000 USD
6. Closures vs Classes¶
The same functionality can be implemented with closures or classes.
Closure Version¶
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
c = make_counter()
print(c()) # 1
print(c()) # 2
Class Version¶
class Counter:
def __init__(self):
self.count = 0
def __call__(self):
self.count += 1
return self.count
c = Counter()
print(c()) # 1
print(c()) # 2
When to Use What?¶
| Situation | Recommendation |
|---|---|
| Simple state maintenance | Closure |
| Multiple methods needed | Class |
| Inheritance/extension needed | Class |
| Functional style | Closure |
| Complex state management | Class |
7. Closure Caveats¶
Loops and Closures¶
# Wrong example
functions = []
for i in range(3):
def f():
return i
functions.append(f)
# All return 2! (last i value)
print([f() for f in functions]) # [2, 2, 2]
Solution 1: Use Default Argument¶
functions = []
for i in range(3):
def f(x=i): # Capture value with default argument
return x
functions.append(f)
print([f() for f in functions]) # [0, 1, 2]
Solution 2: Wrap with Closure¶
def make_func(i):
def f():
return i
return f
functions = [make_func(i) for i in range(3)]
print([f() for f in functions]) # [0, 1, 2]
Solution 3: Use lambda¶
functions = [lambda x=i: x for i in range(3)]
print([f() for f in functions]) # [0, 1, 2]
8. Practical Examples¶
Memoization (Caching)¶
def memoize(func):
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)) # Calculated quickly
Debounce (Limit Consecutive Calls)¶
import time
def debounce(wait):
"""Ignore consecutive calls within specified time"""
def decorator(func):
last_call = [0]
def wrapper(*args, **kwargs):
now = time.time()
if now - last_call[0] >= wait:
last_call[0] = now
return func(*args, **kwargs)
return wrapper
return decorator
@debounce(1.0) # Ignore re-calls within 1 second
def save_data():
print("Data saved")
Retry Logic¶
import time
def retry(max_attempts=3, delay=1):
"""Retry on failure"""
def decorator(func):
def wrapper(*args, **kwargs):
attempts = 0
while attempts < max_attempts:
try:
return func(*args, **kwargs)
except Exception as e:
attempts += 1
if attempts == max_attempts:
raise
print(f"Retry {attempts}/{max_attempts}")
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5)
def unstable_api_call():
import random
if random.random() < 0.7:
raise ConnectionError("Connection failed")
return "Success"
9. Scope-Related Functions¶
locals() and globals()¶
x = 10
def func():
y = 20
print(f"Local variables: {locals()}") # {'y': 20}
print(f"Global variable x: {globals()['x']}") # 10
func()
vars()¶
class MyClass:
def __init__(self):
self.a = 1
self.b = 2
obj = MyClass()
print(vars(obj)) # {'a': 1, 'b': 2}
10. Summary¶
| Keyword/Concept | Description |
|---|---|
| LEGB | Variable search order: Local → Enclosing → Global → Built-in |
| global | Use when modifying global variables |
| nonlocal | Use when modifying enclosing function's variables |
| Closure | Inner function that remembers outer function's environment |
__closure__ |
Check variables referenced by closure |
11. Practice Problems¶
Exercise 1: Counter Factory¶
Create a counter factory that allows setting start value and increment.
# counter = make_counter(start=10, step=5)
# counter() → 10
# counter() → 15
# counter() → 20
Exercise 2: Function Call History¶
Create a closure that records function call history.
# tracked_add, get_history = track_calls(add)
# tracked_add(1, 2)
# tracked_add(3, 4)
# get_history() → [(1, 2, 3), (3, 4, 7)]
Exercise 3: Rate Limiter¶
Create a closure that limits calls per second.
Next Steps¶
Check out 06_Metaclasses.md to learn about metaclasses, the class of classes!