Bài 5: Advanced Functions - First-Class Functions & Closures
Mục Tiêu Bài Học
Sau khi hoàn thành bài này, bạn sẽ:
- ✅ Hiểu first-class functions
- ✅ Làm việc với nested functions
- ✅ Tạo và sử dụng closures
- ✅ Hiểu nonlocal keyword
- ✅ Tạo function factories
- ✅ Áp dụng closure patterns
First-Class Functions
Trong Python, functions là first-class objects - có thể assign, pass, return như objects khác.
Functions as Objects
def greet(name): """Greet someone.""" return f"Hello, {name}!" # Function is an objectprint(type(greet)) # <class 'function'>print(greet.__name__) # greet # Assign to variablesay_hello = greetprint(say_hello("Alice")) # Hello, Alice! # Store in data structuresfunctions = [greet, str.upper, len]for func in functions: print(func) # Function as dictionary valueoperations = { 'greet': greet, 'upper': str.upper, 'length': len} print(operations['greet']("Bob")) # Hello, Bob!
Functions as Arguments
def apply_function(func, value): """Apply function to value.""" return func(value) def square(x): return x ** 2 def cube(x): return x ** 3 # Pass functions as argumentsprint(apply_function(square, 5)) # 25print(apply_function(cube, 3)) # 27print(apply_function(len, "hello")) # 5 # With lambdaprint(apply_function(lambda x: x * 2, 10)) # 20
Functions Returning Functions
def make_multiplier(factor): """Return function that multiplies by factor.""" def multiplier(x): return x * factor return multiplier # Create specific multipliersdouble = make_multiplier(2)triple = make_multiplier(3) print(double(5)) # 10print(triple(5)) # 15 # Different functionsprint(double) # <function make_multiplier.<locals>.multiplier>print(triple) # <function make_multiplier.<locals>.multiplier>print(double is triple) # False
Nested Functions
Nested function là function định nghĩa bên trong function khác.
Basic Nested Function
def outer(): """Outer function.""" message = "Hello from outer" def inner(): """Inner function.""" print(message) # Can access outer's variables inner() # Call inner return "Outer complete" result = outer()# Hello from outer # inner() not accessible here# inner() # NameError: name 'inner' is not defined
Variable Scope
def scope_demo(): """Demonstrate scope.""" outer_var = "outer" def inner(): inner_var = "inner" print(f"Inner can access: {outer_var}") print(f"Inner var: {inner_var}") inner() print(f"Outer var: {outer_var}") # print(inner_var) # NameError - can't access inner's vars scope_demo()# Inner can access: outer# Inner var: inner# Outer var: outer
Returning Nested Functions
def greeting_factory(greeting): """Create greeting functions.""" def greet(name): return f"{greeting}, {name}!" return greet # Create different greetershello = greeting_factory("Hello")hi = greeting_factory("Hi")bonjour = greeting_factory("Bonjour") print(hello("Alice")) # Hello, Alice!print(hi("Bob")) # Hi, Bob!print(bonjour("Marie")) # Bonjour, Marie!
Closures
Closure là nested function có quyền truy cập variables từ outer function, ngay cả sau khi outer function đã return.
What is a Closure?
def make_counter(): """Create counter closure.""" count = 0 # Free variable def counter(): nonlocal count # Access outer variable count += 1 return count return counter # Create counterscounter1 = make_counter()counter2 = make_counter() # Each counter maintains its own stateprint(counter1()) # 1print(counter1()) # 2print(counter1()) # 3 print(counter2()) # 1print(counter2()) # 2 # counter1 and counter2 are independentprint(counter1()) # 4
Closure Inspection
def outer(x): """Outer function.""" def inner(y): return x + y return inner closure = outer(10) # Inspect closureprint(closure.__name__) # innerprint(closure.__closure__) # Cell objectsprint(closure.__closure__[0].cell_contents) # 10 # Use closureprint(closure(5)) # 15print(closure(20)) # 30
nonlocal Keyword
def counter_demo(): """Demonstrate nonlocal.""" count = 0 def increment(): nonlocal count # Modify outer variable count += 1 return count def decrement(): nonlocal count count -= 1 return count def get_count(): return count # Read only - no nonlocal needed return increment, decrement, get_count # Create counter functionsinc, dec, get = counter_demo() print(inc()) # 1print(inc()) # 2print(dec()) # 1print(get()) # 1
Multiple Free Variables
def make_person(name, age): """Create person closure.""" def get_info(): return f"{name} is {age} years old" def have_birthday(): nonlocal age age += 1 return f"Happy birthday! {name} is now {age}" def get_name(): return name def get_age(): return age return { 'info': get_info, 'birthday': have_birthday, 'name': get_name, 'age': get_age } # Create personalice = make_person("Alice", 25) print(alice['info']()) # Alice is 25 years oldprint(alice['birthday']()) # Happy birthday! Alice is now 26print(alice['age']()) # 26print(alice['info']()) # Alice is 26 years old
Function Factories
Function factory là function tạo và return functions khác.
Configuration Factory
def make_logger(level): """Create logger with specific level.""" def log(message): print(f"[{level}] {message}") return log # Create different loggersdebug = make_logger("DEBUG")info = make_logger("INFO")error = make_logger("ERROR") debug("Starting application")info("User logged in")error("Connection failed")# [DEBUG] Starting application# [INFO] User logged in# [ERROR] Connection failed
Validator Factory
def make_validator(min_value, max_value): """Create validator for range.""" def validate(value): if not min_value <= value <= max_value: raise ValueError( f"Value must be between {min_value} and {max_value}" ) return True return validate # Create validatorsvalidate_age = make_validator(0, 120)validate_percentage = make_validator(0, 100) # Use validatorstry: validate_age(25) # OK validate_age(150) # ValueErrorexcept ValueError as e: print(e) # Value must be between 0 and 120 validate_percentage(50) # OK
Formatter Factory
def make_formatter(prefix, suffix): """Create text formatter.""" def format_text(text): return f"{prefix}{text}{suffix}" return format_text # Create formattersbold = make_formatter("<b>", "</b>")italic = make_formatter("<i>", "</i>")code = make_formatter("`", "`") print(bold("Important")) # <b>Important</b>print(italic("Emphasis")) # <i>Emphasis</i>print(code("variable")) # `variable`
Closure Patterns
1. Private Variables
def create_account(initial_balance): """Bank account with private balance.""" balance = initial_balance # Private variable def deposit(amount): nonlocal balance if amount > 0: balance += amount return f"Deposited {amount}. Balance: {balance}" return "Invalid amount" def withdraw(amount): nonlocal balance if 0 < amount <= balance: balance -= amount return f"Withdrew {amount}. Balance: {balance}" return "Invalid amount or insufficient funds" def get_balance(): return balance return { 'deposit': deposit, 'withdraw': withdraw, 'balance': get_balance } # Create accountaccount = create_account(1000) print(account['balance']()) # 1000print(account['deposit'](500)) # Deposited 500. Balance: 1500print(account['withdraw'](200)) # Withdrew 200. Balance: 1300print(account['balance']()) # 1300 # Can't access balance directly# print(balance) # NameError
2. Memoization
def memoize(func): """Memoization using closure.""" cache = {} def wrapper(*args): if args not in cache: print(f"Computing {func.__name__}{args}") cache[args] = func(*args) else: print(f"Cached {func.__name__}{args}") return cache[args] return wrapper @memoizedef fibonacci(n): """Calculate fibonacci number.""" if n < 2: return n return fibonacci(n - 1) + fibonacci(n - 2) # First call computesprint(fibonacci(5))# Computing fibonacci(5)# Computing fibonacci(4)# Computing fibonacci(3)# ...# 5 # Second call uses cacheprint(fibonacci(5)) # Cached fibonacci(5)
3. Configuration Object
def create_config(): """Create configuration closure.""" settings = { 'debug': False, 'timeout': 30, 'retries': 3 } def get(key, default=None): return settings.get(key, default) def set(key, value): settings[key] = value def update(new_settings): settings.update(new_settings) def get_all(): return settings.copy() return { 'get': get, 'set': set, 'update': update, 'all': get_all } # Create configconfig = create_config() print(config['get']('debug')) # Falseconfig['set']('debug', True)print(config['get']('debug')) # True config['update']({'timeout': 60, 'retries': 5})print(config['all']())# {'debug': True, 'timeout': 60, 'retries': 5}
4. Rate Limiter
import time def create_rate_limiter(max_calls, period): """Create rate limiter closure.""" calls = [] def is_allowed(): nonlocal calls now = time.time() # Remove old calls calls = [t for t in calls if t > now - period] # Check limit if len(calls) >= max_calls: return False # Record call calls.append(now) return True return is_allowed # Create limiter: 3 calls per 5 secondscan_call = create_rate_limiter(3, 5) # Testfor i in range(5): if can_call(): print(f"Call {i+1}: Allowed") else: print(f"Call {i+1}: Denied - rate limit exceeded") time.sleep(1)# Call 1: Allowed# Call 2: Allowed# Call 3: Allowed# Call 4: Denied - rate limit exceeded# Call 5: Denied - rate limit exceeded
5. Event Handler
def create_event_handler(): """Create event handler with closures.""" handlers = {} def on(event, handler): """Register event handler.""" if event not in handlers: handlers[event] = [] handlers[event].append(handler) def emit(event, *args, **kwargs): """Trigger event.""" if event in handlers: for handler in handlers[event]: handler(*args, **kwargs) def off(event, handler=None): """Unregister handler.""" if handler is None: handlers.pop(event, None) elif event in handlers: handlers[event].remove(handler) return on, emit, off # Create event systemon, emit, off = create_event_handler() # Register handlersdef on_user_login(username): print(f"User logged in: {username}") def on_user_logout(username): print(f"User logged out: {username}") on('login', on_user_login)on('logout', on_user_logout) # Trigger eventsemit('login', 'alice') # User logged in: aliceemit('logout', 'alice') # User logged out: alice
Real-world Examples
1. Retry Mechanism
import time def create_retry(max_attempts=3, delay=1): """Create retry function.""" def retry(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: print(f"Failed after {max_attempts} attempts") raise print(f"Attempt {attempts} failed: {e}") time.sleep(delay) return wrapper return retry # Use factoryretry_3_times = create_retry(max_attempts=3, delay=2) @retry_3_timesdef unstable_function(): import random if random.random() < 0.7: raise ValueError("Random failure") return "Success!" # Will retry on failure# result = unstable_function()
2. Access Control
def create_access_control(): """Create access control system.""" permissions = {} def grant(user, resource): if user not in permissions: permissions[user] = set() permissions[user].add(resource) def revoke(user, resource): if user in permissions: permissions[user].discard(resource) def check_access(user, resource): return user in permissions and resource in permissions[user] def get_permissions(user): return permissions.get(user, set()).copy() return grant, revoke, check_access, get_permissions # Create systemgrant, revoke, check_access, get_perms = create_access_control() # Grant permissionsgrant('alice', 'read')grant('alice', 'write')grant('bob', 'read') # Check accessprint(check_access('alice', 'write')) # Trueprint(check_access('bob', 'write')) # False # Get permissionsprint(get_perms('alice')) # {'read', 'write'}
3. Middleware Chain
def create_middleware_chain(): """Create middleware processing chain.""" middlewares = [] def use(middleware): """Add middleware to chain.""" middlewares.append(middleware) def process(data): """Process data through middleware chain.""" result = data for middleware in middlewares: result = middleware(result) return result return use, process # Create chainuse, process = create_middleware_chain() # Add middlewaresuse(lambda x: x.strip())use(lambda x: x.lower())use(lambda x: x.replace(' ', '_')) # Processresult = process(" Hello World ")print(result) # hello_world
4. State Machine
def create_state_machine(initial_state): """Create state machine with closures.""" current_state = initial_state transitions = {} def add_transition(from_state, event, to_state): """Add state transition.""" key = (from_state, event) transitions[key] = to_state def trigger(event): """Trigger state transition.""" nonlocal current_state key = (current_state, event) if key in transitions: old_state = current_state current_state = transitions[key] return f"{old_state} -> {event} -> {current_state}" return f"No transition for {event} from {current_state}" def get_state(): """Get current state.""" return current_state return add_transition, trigger, get_state # Create state machineadd_trans, trigger, get_state = create_state_machine('idle') # Define transitionsadd_trans('idle', 'start', 'running')add_trans('running', 'pause', 'paused')add_trans('paused', 'resume', 'running')add_trans('running', 'stop', 'idle') # Use state machineprint(get_state()) # idleprint(trigger('start')) # idle -> start -> runningprint(trigger('pause')) # running -> pause -> pausedprint(trigger('resume')) # paused -> resume -> runningprint(trigger('stop')) # running -> stop -> idle
5. Cache with TTL
import time def create_ttl_cache(ttl_seconds=60): """Create cache with time-to-live.""" cache = {} timestamps = {} def get(key): """Get value from cache.""" if key in cache: # Check if expired if time.time() - timestamps[key] < ttl_seconds: return cache[key] else: # Expired - remove del cache[key] del timestamps[key] return None def set(key, value): """Set value in cache.""" cache[key] = value timestamps[key] = time.time() def clear(): """Clear entire cache.""" cache.clear() timestamps.clear() def size(): """Get cache size.""" return len(cache) return get, set, clear, size # Create cache with 5-second TTLget, set, clear, size = create_ttl_cache(ttl_seconds=5) # Use cacheset('user1', {'name': 'Alice', 'age': 25})print(get('user1')) # {'name': 'Alice', 'age': 25} time.sleep(6)print(get('user1')) # None (expired)
Best Practices
# 1. Use nonlocal for modifying outer variablesdef counter(): count = 0 def increment(): nonlocal count # Required to modify count += 1 return count return increment # 2. Document closure behaviordef make_multiplier(factor): """ Create multiplier function. Args: factor: Number to multiply by Returns: Function that multiplies input by factor """ def multiply(x): return x * factor return multiply # 3. Keep closures simpledef simple_closure(x): def inner(y): return x + y # Simple, clear return inner # 4. Avoid mutable default arguments in closuresdef bad_closure(items=[]): # Don't do this! def add(item): items.append(item) return items return add def good_closure(items=None): # Better if items is None: items = [] def add(item): items.append(item) return items return add # 5. Use closures for data encapsulationdef create_counter(): """Encapsulate counter state.""" count = 0 # Private def increment(): nonlocal count count += 1 return count return increment # Only expose what's needed
Bài Tập Thực Hành
Bài 1: Shopping Cart
Tạo shopping cart closure với add, remove, total, và clear methods.
Bài 2: Throttle Function
Tạo throttle function factory giới hạn execution rate.
Bài 3: Undo/Redo System
Tạo undo/redo system using closures.
Bài 4: Observable Pattern
Tạo observable với subscribe/unsubscribe using closures.
Bài 5: Pipeline Builder
Tạo function pipeline builder with closures.
Tóm Tắt
✅ First-class functions: Functions là objects
✅ Nested functions: Functions bên trong functions
✅ Closures: Functions "remember" outer scope
✅ nonlocal: Modify outer function variables
✅ Function factories: Functions tạo functions
✅ Patterns: Memoization, private variables, configuration
✅ Real-world: Retry, access control, state machines, cache
Bài Tiếp Theo
Bài 5.2: Advanced Functions (Part 2) - *args, **kwargs, partial, functools, và function attributes! 🚀
Remember:
- Closures encapsulate state
- Use nonlocal to modify outer variables
- Function factories create customized functions
- Closures enable data privacy
- Keep closures simple and focused! 🎯