Python Variable Scope Functions vs Loops Code Examples
Practical code examples showing why Python variable scope behaves differently inside functions and loops. Solutions for closure issues and late binding problems.
Python Variable Scope Functions vs Loops Code Examples
This collection demonstrates why Python variable scope behaves differently inside functions and loops through practical code examples. Learn how to handle closure issues, late binding problems, and scope resolution with working solutions.
Basic Scope Behavior Comparison #
Function Scope vs Loop Scope #
# Function scope - creates new namespace each time
def function_scope_demo():
def create_function(value):
x = value # New local variable each call
def inner():
return f"Function x: {x}"
return inner
functions = []
for i in range(3):
functions.append(create_function(i))
return functions
# Loop scope - shares namespace
def loop_scope_demo():
functions = []
for i in range(3): # Same 'i' variable throughout
def inner():
return f"Loop i: {i}" # Captures by reference
functions.append(inner)
return functions
# Test both approaches
print("Function scope results:")
func_results = function_scope_demo()
for f in func_results:
print(f())
print("\nLoop scope results:")
loop_results = loop_scope_demo()
for f in loop_results:
print(f())
Variable Identity Tracking #
def track_variable_identity():
"""Track how variables are captured in different contexts"""
# Function parameter creates new binding
def with_parameters():
functions = []
def make_func(n): # Parameter creates new scope
print(f"Creating function with n={n}, id={id(n)}")
def inner():
print(f"Executing with n={n}, id={id(n)}")
return n
return inner
for i in range(3):
functions.append(make_func(i))
return functions
# Loop variable shares binding
def with_loop_variable():
functions = []
for i in range(3): # Same variable 'i'
print(f"Creating function with i={i}, id={id(i)}")
def inner():
print(f"Executing with i={i}, id={id(i)}")
return i
functions.append(inner)
return functions
print("=== Parameter-based functions ===")
param_funcs = with_parameters()
for f in param_funcs:
f()
print("\n=== Loop variable functions ===")
loop_funcs = with_loop_variable()
for f in loop_funcs:
f()
track_variable_identity()
Late Binding Solutions #
Method 1: Default Arguments #
def late_binding_solutions():
"""Different ways to solve late binding issues"""
# Problem: Late binding
print("=== Problem: Late Binding ===")
bad_functions = []
for i in range(4):
bad_functions.append(lambda: i * i)
print("Bad functions (all return same value):")
for f in bad_functions:
print(f())
# Solution 1: Default arguments
print("\n=== Solution 1: Default Arguments ===")
good_functions_1 = []
for i in range(4):
good_functions_1.append(lambda x=i: x * x)
print("Good functions with default args:")
for f in good_functions_1:
print(f())
# Solution 2: Closure factory
print("\n=== Solution 2: Closure Factory ===")
def make_squarer(n):
return lambda: n * n
good_functions_2 = []
for i in range(4):
good_functions_2.append(make_squarer(i))
print("Good functions with factory:")
for f in good_functions_2:
print(f())
# Solution 3: Immediate execution
print("\n=== Solution 3: Immediate Execution ===")
good_functions_3 = []
for i in range(4):
good_functions_3.append((lambda x: lambda: x * x)(i))
print("Good functions with immediate execution:")
for f in good_functions_3:
print(f())
late_binding_solutions()
Advanced Closure Patterns #
def advanced_closure_patterns():
"""Advanced patterns for managing closures in loops"""
# Pattern 1: Counter factory with private state
def create_counter_factory():
counters = []
for i in range(3):
def make_counter(start_value):
count = start_value
def increment():
nonlocal count
count += 1
return count
def get_count():
return count
def reset():
nonlocal count
count = start_value
return count
return increment, get_count, reset
counters.append(make_counter(i * 10))
return counters
print("=== Counter Factory Pattern ===")
counters = create_counter_factory()
for i, (inc, get, reset) in enumerate(counters):
print(f"Counter {i}: start={get()}, inc={inc()}, inc={inc()}, reset={reset()}")
# Pattern 2: Configuration closures
def create_processors():
configs = [
{'operation': 'multiply', 'factor': 2},
{'operation': 'add', 'value': 10},
{'operation': 'power', 'exponent': 3}
]
processors = []
for config in configs:
def make_processor(cfg): # Parameter captures config
def process(x):
if cfg['operation'] == 'multiply':
return x * cfg['factor']
elif cfg['operation'] == 'add':
return x + cfg['value']
elif cfg['operation'] == 'power':
return x ** cfg['exponent']
return x
return process
processors.append(make_processor(config))
return processors
print("\n=== Configuration Closure Pattern ===")
processors = create_processors()
test_value = 5
for i, proc in enumerate(processors):
result = proc(test_value)
print(f"Processor {i}: {test_value} -> {result}")
advanced_closure_patterns()
Scope Resolution Examples #
LEGB Rule Demonstration #
def legb_rule_examples():
"""Demonstrate LEGB (Local, Enclosing, Global, Built-in) rule"""
# Global variables
global_var = "I'm global"
def outer_function():
# Enclosing scope
enclosing_var = "I'm enclosing"
def inner_function():
# Local scope
local_var = "I'm local"
# Accessing variables following LEGB rule
print(f"Local: {local_var}")
print(f"Enclosing: {enclosing_var}")
print(f"Global: {global_var}")
print(f"Built-in: {len('built-in function')}") # len is built-in
# Show scope resolution with same names
def scope_shadowing_demo():
x = "local x" # Local
def nested():
x = "nested x" # Shadows enclosing
print(f"Nested sees: {x}")
nested()
print(f"Function sees: {x}")
scope_shadowing_demo()
inner_function()
print("=== LEGB Rule Demonstration ===")
outer_function()
# Variable resolution in loops
def loop_scope_resolution():
functions = []
x = "enclosing x"
for i in range(3):
x = f"loop x {i}" # Modifies enclosing x
def capture_current():
current_x = x # Captures current value
def inner():
return current_x
return inner
def capture_reference():
def inner():
return x # Captures by reference
return inner
functions.append(('current', capture_current()))
functions.append(('reference', capture_reference()))
return functions, x
print("\n=== Loop Scope Resolution ===")
funcs, final_x = loop_scope_resolution()
print(f"Final x value: {final_x}")
for i in range(0, len(funcs), 2): # Process in pairs
current_func = funcs[i][1]
ref_func = funcs[i+1][1]
print(f"Iteration {i//2}: current={current_func()}, reference={ref_func()}")
legb_rule_examples()
Nonlocal and Global in Loops #
def scope_modification_examples():
"""Examples of modifying variables in different scopes from loops"""
# Global modification from loop functions
counter = 0
def create_global_modifiers():
functions = []
for i in range(3):
def make_modifier(increment):
def modify():
global counter
counter += increment
return counter
return modify
functions.append(make_modifier(i + 1))
return functions
print("=== Global Variable Modification ===")
print(f"Initial counter: {counter}")
global_modifiers = create_global_modifiers()
for i, modifier in enumerate(global_modifiers):
result = modifier()
print(f"Modifier {i} result: {result}")
# Nonlocal modification patterns
def create_enclosing_modifiers():
shared_state = {'value': 100}
individual_states = []
for i in range(3):
individual_state = {'value': i * 10}
individual_states.append(individual_state)
def make_shared_modifier(idx):
def modify_shared(delta):
nonlocal shared_state
shared_state['value'] += delta
return f"Modifier {idx}: shared={shared_state['value']}"
return modify_shared
def make_individual_modifier(state, idx):
def modify_individual(delta):
state['value'] += delta
return f"Modifier {idx}: individual={state['value']}"
return modify_individual
shared_modifiers = [make_shared_modifier(i) for i in range(3)]
individual_modifiers = [make_individual_modifier(state, i)
for i, state in enumerate(individual_states)]
return shared_modifiers, individual_modifiers
print("\n=== Enclosing Scope Modification ===")
shared_mods, individual_mods = create_enclosing_modifiers()
print("Shared state modifications:")
for modifier in shared_mods:
print(modifier(5))
print("\nIndividual state modifications:")
for modifier in individual_mods:
print(modifier(3))
scope_modification_examples()
Mutable Object Behavior #
Shared vs Private Mutable State #
def mutable_object_examples():
"""Examples showing mutable object behavior in closures"""
# Problem: Shared mutable objects
def shared_mutable_problem():
shared_list = []
functions = []
for i in range(3):
def append_to_shared(value):
shared_list.append(value)
return shared_list.copy() # Return copy to see current state
functions.append(append_to_shared)
return functions
print("=== Shared Mutable Object Problem ===")
shared_funcs = shared_mutable_problem()
print("Each function modifies the same list:")
for i, func in enumerate(shared_funcs):
result = func(f"item_{i}")
print(f"Function {i} result: {result}")
# Solution: Private mutable objects
def private_mutable_solution():
functions = []
for i in range(3):
def create_private_list():
private_list = [] # Each closure gets its own list
def append_to_private(value):
private_list.append(value)
return private_list.copy()
def get_private():
return private_list.copy()
def clear_private():
private_list.clear()
return private_list.copy()
return append_to_private, get_private, clear_private
functions.append(create_private_list())
return functions
print("\n=== Private Mutable Object Solution ===")
private_funcs = private_mutable_solution()
print("Each function has its own list:")
for i, (append_func, get_func, clear_func) in enumerate(private_funcs):
append_func(f"item_{i}")
append_func(f"item_{i}_2")
result = get_func()
print(f"Function {i} list: {result}")
# Advanced: State management with closures
def create_state_managers():
managers = []
for i in range(3):
def create_manager(manager_id):
state = {
'id': manager_id,
'data': [],
'metadata': {'created': f"manager_{manager_id}", 'operations': 0}
}
def add_data(item):
state['data'].append(item)
state['metadata']['operations'] += 1
return state.copy()
def get_state():
return state.copy()
def reset_state():
state['data'].clear()
state['metadata']['operations'] = 0
return state.copy()
return {
'add': add_data,
'get': get_state,
'reset': reset_state
}
managers.append(create_manager(i))
return managers
print("\n=== Advanced State Management ===")
state_managers = create_state_managers()
for i, manager in enumerate(state_managers):
manager['add'](f"data_{i}_1")
manager['add'](f"data_{i}_2")
state = manager['get']()
print(f"Manager {i}: {state}")
mutable_object_examples()
Debugging and Inspection Tools #
Scope Inspection Utilities #
import sys
import inspect
def scope_debugging_tools():
"""Tools for debugging scope issues in functions and loops"""
def inspect_closure_variables():
"""Inspect what variables are captured by closures"""
functions = []
debug_info = []
for i in range(3):
x = i * 10 # Local to loop iteration
def create_inspector(loop_var, local_var):
def inspector():
# Inspect current frame
frame = inspect.currentframe()
# Get closure variables
closure_vars = {}
if inspector.__closure__:
var_names = inspector.__code__.co_freevars
for name, cell in zip(var_names, inspector.__closure__):
try:
closure_vars[name] = cell.cell_contents
except ValueError:
closure_vars[name] = "<empty cell>"
return {
'loop_var': loop_var,
'local_var': local_var,
'closure_vars': closure_vars,
'local_vars': frame.f_locals.copy()
}
return inspector
inspector = create_inspector(i, x)
functions.append(inspector)
# Debug info at creation time
debug_info.append({
'creation_time': {'i': i, 'x': x},
'function': inspector
})
return functions, debug_info
print("=== Closure Variable Inspection ===")
inspectors, creation_info = inspect_closure_variables()
for i, (creation, inspector) in enumerate(zip(creation_info, inspectors)):
print(f"\nInspector {i}:")
print(f" Created with: {creation['creation_time']}")
runtime_info = inspector()
print(f" Runtime info: {runtime_info}")
# Variable lifetime tracking
def track_variable_lifetime():
"""Track how long variables live in different scopes"""
def function_scope_lifetime():
results = []
for i in range(3):
def create_tracker(iteration):
created_at = f"iteration_{iteration}"
def tracker():
return f"Created at {created_at}, iteration was {iteration}"
return tracker
results.append(create_tracker(i))
return results
def loop_scope_lifetime():
results = []
for i in range(3):
created_at = f"iteration_{i}" # This gets overwritten
def tracker():
return f"Created at {created_at}, i is now {i}"
results.append(tracker)
return results
print("\n=== Variable Lifetime Tracking ===")
print("Function scope (each call creates new variables):")
func_trackers = function_scope_lifetime()
for j, tracker in enumerate(func_trackers):
print(f" Tracker {j}: {tracker()}")
print("\nLoop scope (variables overwritten each iteration):")
loop_trackers = loop_scope_lifetime()
for j, tracker in enumerate(loop_trackers):
print(f" Tracker {j}: {tracker()}")
track_variable_lifetime()
scope_debugging_tools()
Performance Implications #
Memory Usage Patterns #
import sys
import gc
def performance_analysis():
"""Analyze performance implications of different scope patterns"""
def memory_usage_comparison():
"""Compare memory usage of different closure patterns"""
# Pattern 1: Late binding (memory efficient but incorrect behavior)
def late_binding_pattern():
functions = []
for i in range(1000):
functions.append(lambda: i) # All share same variable
return functions
# Pattern 2: Default arguments (correct behavior, moderate memory)
def default_arg_pattern():
functions = []
for i in range(1000):
functions.append(lambda x=i: x) # Each captures its own value
return functions
# Pattern 3: Factory functions (correct behavior, more memory)
def factory_pattern():
functions = []
for i in range(1000):
def make_func(n):
def inner():
return n
return inner
functions.append(make_func(i))
return functions
print("=== Memory Usage Comparison ===")
# Test each pattern
patterns = [
("Late Binding", late_binding_pattern),
("Default Arguments", default_arg_pattern),
("Factory Functions", factory_pattern)
]
for name, pattern_func in patterns:
# Force garbage collection before measurement
gc.collect()
# Create functions
functions = pattern_func()
# Measure approximate memory usage
total_size = sum(sys.getsizeof(f) for f in functions)
avg_size = total_size / len(functions)
print(f"{name}:")
print(f" Total size: {total_size} bytes")
print(f" Average per function: {avg_size:.2f} bytes")
print(f" First function result: {functions[0]()}")
print(f" Last function result: {functions[-1]()}")
print()
# Clean up
del functions
gc.collect()
memory_usage_comparison()
# Performance timing
import time
def timing_comparison():
"""Compare execution timing of different patterns"""
def time_pattern(name, create_func, iterations=10000):
# Time function creation
start_time = time.time()
functions = create_func()
creation_time = time.time() - start_time
# Time function execution
start_time = time.time()
for _ in range(iterations):
for f in functions[:100]: # Test first 100 functions
f()
execution_time = time.time() - start_time
print(f"{name}:")
print(f" Creation time: {creation_time:.6f} seconds")
print(f" Execution time: {execution_time:.6f} seconds ({iterations * 100} calls)")
print()
return functions
print("=== Performance Timing Comparison ===")
# Test patterns
late_binding = lambda: [lambda: i for i in range(100)]
default_args = lambda: [lambda x=i: x for i in range(100)]
factory = lambda: [(lambda n: lambda: n)(i) for i in range(100)]
time_pattern("Late Binding", late_binding)
time_pattern("Default Arguments", default_args)
time_pattern("Factory Functions", factory)
timing_comparison()
performance_analysis()
Real-World Applications #
Event Handler Registration #
def event_handler_examples():
"""Real-world examples: Event handler registration patterns"""
class EventSystem:
def __init__(self):
self.handlers = {}
def register(self, event_type, handler):
if event_type not in self.handlers:
self.handlers[event_type] = []
self.handlers[event_type].append(handler)
def trigger(self, event_type, data=None):
if event_type in self.handlers:
results = []
for handler in self.handlers[event_type]:
results.append(handler(data))
return results
return []
# Problem: Incorrect handler registration
def register_handlers_wrong(event_system):
handlers = ['click', 'hover', 'focus']
for handler_name in handlers:
# Wrong: all handlers will use the last value of handler_name
event_system.register(handler_name,
lambda data: f"Wrong {handler_name} handler: {data}")
# Solution: Correct handler registration
def register_handlers_correct(event_system):
handlers = ['click', 'hover', 'focus']
for handler_name in handlers:
# Correct: capture current value with default argument
event_system.register(handler_name,
lambda data, name=handler_name: f"Correct {name} handler: {data}")
# Alternative: Factory function approach
def register_handlers_factory(event_system):
handlers = ['click', 'hover', 'focus']
def create_handler(name):
def handler(data):
return f"Factory {name} handler: {data}"
return handler
for handler_name in handlers:
event_system.register(handler_name, create_handler(handler_name))
print("=== Event Handler Registration ===")
# Test wrong approach
wrong_system = EventSystem()
register_handlers_wrong(wrong_system)
print("Wrong approach results:")
for event in ['click', 'hover', 'focus']:
results = wrong_system.trigger(event, f"{event}_data")
print(f" {event}: {results}")
# Test correct approach
correct_system = EventSystem()
register_handlers_correct(correct_system)
print("\nCorrect approach results:")
for event in ['click', 'hover', 'focus']:
results = correct_system.trigger(event, f"{event}_data")
print(f" {event}: {results}")
# Test factory approach
factory_system = EventSystem()
register_handlers_factory(factory_system)
print("\nFactory approach results:")
for event in ['click', 'hover', 'focus']:
results = factory_system.trigger(event, f"{event}_data")
print(f" {event}: {results}")
event_handler_examples()
Dynamic Function Generation #
def dynamic_function_examples():
"""Examples of dynamically generating functions with proper scope handling"""
def create_validators():
"""Create form field validators dynamically"""
validation_rules = [
{'field': 'email', 'pattern': r'.*@.*', 'message': 'Invalid email'},
{'field': 'phone', 'pattern': r'\d{10}', 'message': 'Invalid phone'},
{'field': 'zip', 'pattern': r'\d{5}', 'message': 'Invalid zip code'}
]
validators = {}
# Wrong way: late binding issue
wrong_validators = {}
for rule in validation_rules:
import re
wrong_validators[rule['field']] = lambda value: (
re.match(rule['pattern'], value) is not None,
rule['message'] # This will always use the last rule's message!
)
# Correct way: capture rule with factory function
for rule in validation_rules:
def create_validator(validation_rule):
import re
compiled_pattern = re.compile(validation_rule['pattern'])
def validator(value):
is_valid = compiled_pattern.match(str(value)) is not None
return is_valid, validation_rule['message']
return validator
validators[rule['field']] = create_validator(rule)
return wrong_validators, validators
print("=== Dynamic Validator Generation ===")
wrong_validators, correct_validators = create_validators()
test_data = {
'email': 'test@example.com',
'phone': '1234567890',
'zip': '12345'
}
print("Wrong validators (all use same message):")
for field, value in test_data.items():
is_valid, message = wrong_validators[field](value)
print(f" {field}: valid={is_valid}, message='{message}'")
print("\nCorrect validators:")
for field, value in test_data.items():
is_valid, message = correct_validators[field](value)
print(f" {field}: valid={is_valid}, message='{message}'")
# Advanced: Creating configurable processors
def create_data_processors():
"""Create data processing functions with different configurations"""
processor_configs = [
{'name': 'normalize', 'func': lambda x: x / 100, 'description': 'Normalize to 0-1'},
{'name': 'square', 'func': lambda x: x ** 2, 'description': 'Square the value'},
{'name': 'logarithm', 'func': lambda x: __import__('math').log(x) if x > 0 else 0, 'description': 'Natural log'}
]
processors = {}
for config in processor_configs:
def create_processor(cfg):
def processor(data):
if isinstance(data, (list, tuple)):
return [cfg['func'](item) for item in data]
else:
return cfg['func'](data)
# Add metadata to the function
processor.name = cfg['name']
processor.description = cfg['description']
return processor
processors[config['name']] = create_processor(config)
return processors
print("\n=== Dynamic Data Processors ===")
processors = create_data_processors()
test_data = [1, 4, 9, 16, 25]
for name, processor in processors.items():
try:
result = processor(test_data)
print(f"{processor.name} ({processor.description}): {result}")
except Exception as e:
print(f"{processor.name}: Error - {e}")
dynamic_function_examples()
Summary #
These examples demonstrate the key differences in how Python handles variable scope inside functions versus loops:
Core Concepts:
- Functions create new scope contexts, loops share existing scope
- Closures capture variables by reference, leading to late binding issues
- Different solutions exist for proper variable capture in loops
Common Patterns:
- Use default arguments to capture current values
- Factory functions for clean closure creation
- Proper state management with nonlocal and global
- Debugging techniques for scope issues
Best Practices:
- Prefer explicit parameter passing over closure capture
- Use factory functions for complex closure scenarios
- Consider performance implications of different patterns
- Test closure behavior thoroughly in loops
Related Topics: