Practical Patterns¶
클로저의 실용적인 활용 패턴과 디버깅 방법입니다.
Factory Functions¶
Basic Factory¶
def make_multiplier(n):
def multiply(x):
return x * n
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
Configuration Factory¶
def make_formatter(prefix, suffix=""):
def format(value):
return f"{prefix}{value}{suffix}"
return format
currency = make_formatter("$", " USD")
percent = make_formatter("", "%")
print(currency(100)) # \$100 USD
print(percent(75)) # 75%
Validator Factory¶
def make_validator(min_val, max_val):
def validate(value):
if min_val <= value <= max_val:
return True
raise ValueError(f"Value must be between {min_val} and {max_val}")
return validate
validate_age = make_validator(0, 150)
validate_percent = make_validator(0, 100)
Decorators with State¶
Call Counter¶
def count_calls(func):
count = 0
def wrapper(*args, **kwargs):
nonlocal count
count += 1
print(f"Call #{count}")
return func(*args, **kwargs)
wrapper.get_count = lambda: count
return wrapper
@count_calls
def greet(name):
return f"Hello, {name}!"
greet("Alice") # Call #1
greet("Bob") # Call #2
print(greet.get_count()) # 2
Memoization¶
def memoize(func):
cache = {}
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
wrapper.cache = cache
wrapper.clear = lambda: cache.clear()
return wrapper
@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(100)) # Fast!
Rate Limiter¶
import time
def rate_limit(max_calls, period):
calls = []
def decorator(func):
def wrapper(*args, **kwargs):
now = time.time()
# Remove old calls
while calls and calls[0] < now - period:
calls.pop(0)
if len(calls) >= max_calls:
raise Exception("Rate limit exceeded")
calls.append(now)
return func(*args, **kwargs)
return wrapper
return decorator
@rate_limit(max_calls=3, period=60)
def api_call():
return "Response"
functools.partial¶
기존 함수의 인자를 고정합니다:
from functools import partial
def power(base, exponent):
return base ** exponent
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(5)) # 25
print(cube(5)) # 125
partial vs Closure¶
# Using closure
def make_power(exp):
return lambda x: x ** exp
# Using partial
from functools import partial
def power(x, exp):
return x ** exp
square_closure = make_power(2)
square_partial = partial(power, exp=2)
# Both work the same
print(square_closure(5)) # 25
print(square_partial(5)) # 25
| Approach | Pros | Cons |
|---|---|---|
| Closure | Full control, custom logic | More verbose |
partial |
Concise, preserves metadata | Only fixes arguments |
Callback Patterns¶
Event Handler Factory¶
def make_handler(event_name, callback):
def handler(data):
print(f"[{event_name}] Processing...")
return callback(data)
return handler
def process_click(data):
return f"Clicked at {data['x']}, {data['y']}"
click_handler = make_handler("CLICK", process_click)
print(click_handler({'x': 100, 'y': 200}))
Retry Logic¶
import time
def with_retry(max_attempts, delay):
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
time.sleep(delay)
return wrapper
return decorator
@with_retry(max_attempts=3, delay=1)
def unstable_api_call():
# Might fail
pass
Debugging Tools¶
Inspect Closure Contents¶
def outer():
x = 10
y = "hello"
return lambda: (x, y)
f = outer()
# View closure
print(f.__closure__) # (<cell ...>, <cell ...>)
# View free variable names
print(f.__code__.co_freevars) # ('x', 'y')
# View cell contents
for var, cell in zip(f.__code__.co_freevars, f.__closure__):
print(f"{var} = {cell.cell_contents}")
# x = 10
# y = hello
Debug Helper Function¶
def inspect_closure(func):
"""Print closure details for debugging."""
print(f"Function: {func.__name__}")
if func.__closure__ is None:
print(" No closure")
return
freevars = func.__code__.co_freevars
for var, cell in zip(freevars, func.__closure__):
print(f" {var} = {cell.cell_contents!r}")
# Usage
def make_adder(n):
return lambda x: x + n
add5 = make_adder(5)
inspect_closure(add5)
# Function: <lambda>
# n = 5
Memory Considerations¶
Problem: Large Captures¶
# Bad: captures entire large_data
def make_handler():
large_data = [0] * 1_000_000
def handler():
return len(large_data) # Only needs length
return handler # Keeps 1M integers alive!
Solution: Capture Only What's Needed¶
# Good: captures only the length
def make_handler():
large_data = [0] * 1_000_000
data_len = len(large_data)
def handler():
return data_len
return handler # large_data can be garbage collected
Avoid Circular References¶
# Potential issue
def outer():
x = []
def inner():
return x
x.append(inner) # Cycle: inner → x → inner
return inner
# Better: use weakref if needed
import weakref
def outer():
x = []
def inner():
return x
# Don't create cycles, or use weak references
return inner
Summary¶
| Pattern | Use Case | Key Technique |
|---|---|---|
| Factory | Create configured functions | Return inner function |
| Decorator | Add behavior to functions | nonlocal for state |
partial |
Fix function arguments | functools.partial |
| Callback | Event handling | Capture context |
| Memoization | Cache results | Dict in closure |
Debugging Checklist:
1. func.__closure__ — Cell objects
2. func.__code__.co_freevars — Variable names
3. cell.cell_contents — Actual values
Runnable Example: closure_vs_class_comparison.py¶
"""
Closures vs Classes - Comparing Two Approaches to Stateful Objects
This tutorial compares implementing the same functionality using
closures and classes, showing when to use each approach.
Run this file to see the comparison in action!
"""
if __name__ == "__main__":
print("=" * 70)
print("CLOSURES VS CLASSES - COMPARISON")
print("=" * 70)
# ============================================================================
# EXAMPLE 1: Understanding the Problem
# ============================================================================
print("\n1. THE PROBLEM - HOW TO MAINTAIN STATE?")
print("-" * 70)
print("""
We want to compute a running average of numbers.
Each call should:
- Accept a new number
- Remember all previous numbers
- Return the average of all numbers seen so far
We can solve this two ways:
1. Using a CLOSURE with captured mutable state
2. Using a CLASS with instance variables
Let's compare both approaches!
""")
# ============================================================================
# EXAMPLE 2: The Closure Approach
# ============================================================================
print("\n2. THE CLOSURE APPROACH")
print("-" * 70)
def make_averager_closure():
"""
Factory function that returns a closure.
The returned function captures a list from the enclosing scope.
"""
series = [] # Captured by the closure
def averager(new_value):
series.append(new_value)
total = sum(series)
return total / len(series)
return averager
print("\nDefined make_averager_closure():\n")
print("""
def make_averager_closure():
series = [] # Captured variable
def averager(new_value):
series.append(new_value)
total = sum(series)
return total / len(series)
return averager
""")
print("How it works:")
print("1. make_averager_closure() creates a new 'series' list")
print("2. It returns the inner 'averager' function")
print("3. averager captures 'series' in its closure")
print("4. Each call to averager modifies the captured list")
print("5. State persists between calls!\n")
avg_closure = make_averager_closure()
print("Created: avg_closure = make_averager_closure()\n")
print("Using the closure:")
print(f" avg_closure(10) = {avg_closure(10)}")
print(f" avg_closure(11) = {avg_closure(11)}")
print(f" avg_closure(12) = {avg_closure(12)}")
# ============================================================================
# EXAMPLE 3: The Class Approach
# ============================================================================
print("\n3. THE CLASS APPROACH")
print("-" * 70)
class Averager:
"""
A class to compute running average.
Uses an instance variable to maintain state.
"""
def __init__(self):
"""Initialize with an empty series list."""
self.series = []
def __call__(self, new_value):
"""
Make instances callable using __call__.
This allows instances to be used like functions!
"""
self.series.append(new_value)
total = sum(self.series)
return total / len(self.series)
print("\nDefined Averager class:\n")
print("""
class Averager:
def __init__(self):
self.series = []
def __call__(self, new_value):
self.series.append(new_value)
total = sum(self.series)
return total / len(self.series)
""")
print("How it works:")
print("1. __init__ creates an empty series list as instance variable")
print("2. __call__ makes instances callable like functions")
print("3. Each call modifies self.series")
print("4. State persists between calls!\n")
avg_class = Averager()
print("Created: avg_class = Averager()\n")
print("Using the instance (called like a function):")
print(f" avg_class(10) = {avg_class(10)}")
print(f" avg_class(11) = {avg_class(11)}")
print(f" avg_class(12) = {avg_class(12)}")
# ============================================================================
# EXAMPLE 4: They Do the Same Thing
# ============================================================================
print("\n4. THEY PRODUCE THE SAME RESULTS")
print("-" * 70)
print("\nBoth approaches compute the exact same average!\n")
avg_c = make_averager_closure()
avg_o = Averager()
values = [10, 11, 12]
print("Using closure approach:")
closure_results = []
for val in values:
result = avg_c(val)
closure_results.append(result)
print(f" {val} -> {result}")
print("\nUsing class approach:")
class_results = []
for val in values:
result = avg_o(val)
class_results.append(result)
print(f" {val} -> {result}")
print(f"\nResults match: {closure_results == class_results}")
# ============================================================================
# EXAMPLE 5: Differences in Syntax
# ============================================================================
print("\n5. DIFFERENCES IN SYNTAX AND USAGE")
print("-" * 70)
print("""
CLOSURE:
avg = make_averager_closure()
result = avg(10) # Function call
CLASS:
avg = Averager()
result = avg(10) # Also works with __call__
At the call site, they look the same!
But the implementation is different.
""")
# ============================================================================
# EXAMPLE 6: Accessing State
# ============================================================================
print("\n6. ACCESSING AND INSPECTING STATE")
print("-" * 70)
avg_c = make_averager_closure()
avg_o = Averager()
# Make some calls
for val in [10, 11, 12]:
avg_c(val)
avg_o(val)
print("Accessing state from CLOSURE:\n")
print("Direct access: NOT POSSIBLE")
print(" avg_c.series -> AttributeError!")
print(" You can't directly access captured variables\n")
print("But we can inspect the closure:")
print(f" avg_c.__closure__ = {avg_c.__closure__}")
print(f" avg_c.__closure__[0].cell_contents = {avg_c.__closure__[0].cell_contents}\n")
print("Accessing state from CLASS:\n")
print("Direct access: POSSIBLE!")
print(f" avg_o.series = {avg_o.series}")
print(f" You can read and modify instance variables directly!\n")
print("WHY THIS MATTERS:")
print("- Class state is directly accessible and inspectable")
print("- Closure state is hidden (encapsulation)")
print("- For debugging, classes are easier to work with")
# ============================================================================
# EXAMPLE 7: Adding Methods
# ============================================================================
print("\n7. ADDING MORE METHODS")
print("-" * 70)
print("\nWhat if we want to add a method to get all values?\n")
print("CLOSURE APPROACH:")
print("Need to return multiple functions or attach methods:\n")
def make_averager_extended():
"""Extended closure with multiple operations."""
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total / len(series)
def get_series():
"""Get a copy of all values."""
return series.copy()
# Attach method to function
averager.get_series = get_series
return averager
avg_ext = make_averager_extended()
print(f"avg_ext(10) = {avg_ext(10)}")
print(f"avg_ext(11) = {avg_ext(11)}")
print(f"avg_ext.get_series() = {avg_ext.get_series()}\n")
print("CLASS APPROACH:")
print("Just add another method:\n")
class AveragerExtended:
"""Extended class with multiple methods."""
def __init__(self):
self.series = []
def __call__(self, new_value):
self.series.append(new_value)
total = sum(self.series)
return total / len(self.series)
def get_series(self):
"""Get a copy of all values."""
return self.series.copy()
avg_ext = AveragerExtended()
print(f"avg_ext(10) = {avg_ext(10)}")
print(f"avg_ext(11) = {avg_ext(11)}")
print(f"avg_ext.get_series() = {avg_ext.get_series()}\n")
print("COMPARISON:")
print("- Closure: Awkward to add multiple operations")
print("- Class: Natural way to add methods")
# ============================================================================
# EXAMPLE 8: When to Use Each Approach
# ============================================================================
print("\n8. WHEN TO USE CLOSURES VS CLASSES")
print("-" * 70)
print("""
USE CLOSURES WHEN:
✓ The object has ONE main operation (is essentially a function)
✓ State is simple and minimal
✓ You want to hide internal state (encapsulation)
✓ The function is returned from a factory (higher-order functions)
✓ You're implementing decorators
✓ You want a lightweight, minimal memory footprint
Examples:
- Running average calculator
- Event listener callback
- Decorator functions
- Filter/transform functions
USE CLASSES WHEN:
✓ The object has MULTIPLE methods
✓ State is complex and needs multiple operations
✓ You need to inherit from other classes
✓ You want a clear, explicit public API
✓ You need to introspect the object
✓ Other developers need to understand the code quickly
Examples:
- Bank account (deposit, withdraw, balance, etc.)
- File reader (read, seek, tell, close)
- Request handler (validate, process, respond)
- Game character (move, attack, defend, heal)
HYBRID APPROACH:
Some objects use both:
- Class with __call__ makes it callable like a function
- But it has multiple methods for different operations
- Best of both worlds!
""")
# ============================================================================
# EXAMPLE 9: Readability and Maintenance
# ============================================================================
print("\n9. READABILITY AND MAINTENANCE")
print("-" * 70)
print("""
CLOSURE READABILITY:
- Compact, minimal code
- Good for simple cases
- Harder to debug (hidden state)
- Hard to introspect
CLASS READABILITY:
- More explicit, self-documenting
- Clear what methods are available
- Easy to debug (visible state)
- Easy to introspect and understand
- Familiar to most programmers
QUOTE FROM PEP 20 (Zen of Python):
"Explicit is better than implicit."
Classes are usually more explicit!
""")
# ============================================================================
# EXAMPLE 10: Performance Comparison
# ============================================================================
print("\n10. PERFORMANCE COMPARISON")
print("-" * 70)
import timeit
# Closure version
def make_avg_c():
series = []
def avg(val):
series.append(val)
return sum(series) / len(series)
return avg
# Class version
class AvgClass:
def __init__(self):
self.series = []
def __call__(self, val):
self.series.append(val)
return sum(self.series) / len(self.series)
avg_c = make_avg_c()
avg_o = AvgClass()
print("\nPerformance test (100,000 calls with value 42):\n")
closure_time = timeit.timeit(lambda: avg_c(42), number=100000)
print(f"Closure approach: {closure_time:.6f} seconds")
class_time = timeit.timeit(lambda: avg_o(42), number=100000)
print(f"Class approach: {class_time:.6f} seconds")
print(f"\nDifference: {abs(closure_time - class_time):.6f} seconds")
print("(Difference is negligible for most applications)")
print("\nCONCLUSION:")
print("- Performance is essentially the same")
print("- Choose based on design, not performance")
print("- Readability matters more than micro-optimizations")
# ============================================================================
# SUMMARY: Making the Choice
# ============================================================================
print("\n" + "=" * 70)
print("SUMMARY - CHOOSING BETWEEN CLOSURES AND CLASSES")
print("=" * 70)
print("""
DECISION FLOWCHART:
1. Does the object have only ONE callable operation?
-> Use closure (simpler, less code)
2. Does the object have MULTIPLE methods?
-> Use class (clearer, more maintainable)
3. Is state complex or needs introspection?
-> Use class (easier to debug)
4. Is this a factory function returning callables?
-> Use closure (natural pattern)
5. Is this a decorator?
-> Use closure (standard pattern)
6. Do you need inheritance?
-> Use class (required)
DEFAULT RECOMMENDATION:
- Start with a class for clarity
- Use closures only when the closure approach is clearly better
- Don't over-engineer simple cases with classes
- Don't hide complexity in closures
PYTHONIC APPROACH:
"Explicit is better than implicit." - Zen of Python
Classes make intent explicit.
Classes are the default choice for most scenarios.
Use closures for specific patterns where they shine:
- Decorators
- Factory functions
- Event handlers
- Simple callbacks
BOTH ARE VALID:
There's no single "right" answer.
Choose based on:
- Clarity and readability
- Maintenance considerations
- Team preferences
- Specific use case requirements
The best code is the code that's easiest to understand,
maintain, and modify by your team!
""")