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! 🎯