Bài 5: Advanced Functions - *args, **kwargs & functools

Mục Tiêu Bài Học

Sau khi hoàn thành bài này, bạn sẽ:

  • ✅ Sử dụng *args và **kwargs
  • ✅ Áp dụng functools module
  • ✅ Sử dụng partial functions
  • ✅ Làm việc với function annotations
  • ✅ Sử dụng type hints
  • ✅ Áp dụng functools decorators

*args và **kwargs

Variable-length arguments cho phép function nhận số lượng arguments linh hoạt.

*args - Positional Arguments

def sum_all(*args):    """Sum all arguments."""    print(f"args type: {type(args)}")  # tuple    print(f"args: {args}")    return sum(args) # Variable number of argumentsprint(sum_all(1, 2, 3))           # 6print(sum_all(1, 2, 3, 4, 5))     # 15print(sum_all(10))                # 10print(sum_all())                  # 0 # Unpack list/tuplenumbers = [1, 2, 3, 4, 5]print(sum_all(*numbers))          # 15

**kwargs - Keyword Arguments

def print_info(**kwargs):    """Print keyword arguments."""    print(f"kwargs type: {type(kwargs)}")  # dict    print(f"kwargs: {kwargs}")        for key, value in kwargs.items():        print(f"{key}: {value}") # Variable keyword argumentsprint_info(name="Alice", age=25, city="NYC")# kwargs type: <class 'dict'># kwargs: {'name': 'Alice', 'age': 25, 'city': 'NYC'}# name: Alice# age: 25# city: NYC # Unpack dictionaryuser = {'name': 'Bob', 'age': 30}print_info(**user)

Combining *args and **kwargs

def flexible_function(required, *args, optional=None, **kwargs):    """Function with all argument types."""    print(f"Required: {required}")    print(f"Args: {args}")    print(f"Optional: {optional}")    print(f"Kwargs: {kwargs}") # Various callsflexible_function(1)# Required: 1, Args: (), Optional: None, Kwargs: {} flexible_function(1, 2, 3)# Required: 1, Args: (2, 3), Optional: None, Kwargs: {} flexible_function(1, 2, 3, optional="value")# Required: 1, Args: (2, 3), Optional: value, Kwargs: {} flexible_function(1, 2, 3, optional="value", extra1="a", extra2="b")# Required: 1, Args: (2, 3), Optional: value, Kwargs: {'extra1': 'a', 'extra2': 'b'}

Argument Order

def argument_order(pos1, pos2, *args, kw1, kw2=None, **kwargs):    """    Correct argument order:    1. Positional arguments    2. *args    3. Keyword-only arguments    4. **kwargs    """    print(f"pos1={pos1}, pos2={pos2}")    print(f"args={args}")    print(f"kw1={kw1}, kw2={kw2}")    print(f"kwargs={kwargs}") # Usageargument_order(1, 2, 3, 4, kw1="required", kw2="optional", extra="value")# pos1=1, pos2=2# args=(3, 4)# kw1=required, kw2=optional# kwargs={'extra': 'value'}

Practical Examples

def create_user(username, email, *permissions, **profile):    """Create user with variable permissions and profile data."""    user = {        'username': username,        'email': email,        'permissions': list(permissions),        'profile': profile    }    return user # Create usersadmin = create_user(    'admin',    '[email protected]',    'read', 'write', 'delete',    age=30,    department='IT') print(admin)# {#     'username': 'admin',#     'email': '[email protected]',#     'permissions': ['read', 'write', 'delete'],#     'profile': {'age': 30, 'department': 'IT'}# } # Wrapper function patterndef logged_function(func):    """Wrapper that preserves all arguments."""    def wrapper(*args, **kwargs):        print(f"Calling {func.__name__}")        result = func(*args, **kwargs)        print(f"Result: {result}")        return result    return wrapper @logged_functiondef multiply(a, b):    return a * b multiply(5, 3)# Calling multiply# Result: 15

functools Module

Module functools cung cấp higher-order functions và operations.

functools.partial

partial tạo function mới với một số arguments pre-filled.

from functools import partial def power(base, exponent):    """Calculate base^exponent."""    return base ** exponent # Create specialized functionssquare = partial(power, exponent=2)cube = partial(power, exponent=3) print(square(5))   # 25print(cube(5))     # 125 # With multiple argumentsdef greet(greeting, name, punctuation):    return f"{greeting}, {name}{punctuation}" hello = partial(greet, "Hello")hello_excited = partial(greet, "Hello", punctuation="!") print(hello("Alice", "."))        # Hello, Alice.print(hello_excited("Bob"))       # Hello, Bob!

Partial for Configuration

from functools import partial def connect_database(host, port, database, username, password):    """Connect to database."""    return {        'host': host,        'port': port,        'database': database,        'username': username,        'password': password,        'connected': True    } # Create pre-configured connectionsconnect_dev = partial(    connect_database,    host='localhost',    port=5432,    database='dev_db') connect_prod = partial(    connect_database,    host='prod.example.com',    port=5432,    database='prod_db') # Use with remaining argumentsdev_conn = connect_dev(username='dev_user', password='dev_pass')prod_conn = connect_prod(username='prod_user', password='prod_pass') print(dev_conn['host'])   # localhostprint(prod_conn['host'])  # prod.example.com

functools.lru_cache

LRU Cache memoizes function results (Least Recently Used).

from functools import lru_cache @lru_cache(maxsize=128)def fibonacci(n):    """Fibonacci with caching."""    print(f"Computing fib({n})")    if n < 2:        return n    return fibonacci(n - 1) + fibonacci(n - 2) # First call - computesprint(fibonacci(5))# Computing fib(5)# Computing fib(4)# Computing fib(3)# Computing fib(2)# Computing fib(1)# Computing fib(0)# 5 # Second call - cachedprint(fibonacci(5))  # No output - fully cached# 5 # Cache infoprint(fibonacci.cache_info())# CacheInfo(hits=8, misses=6, maxsize=128, currsize=6) # Clear cachefibonacci.cache_clear()

lru_cache with Parameters

from functools import lru_cacheimport time @lru_cache(maxsize=32)def expensive_computation(n):    """Simulate expensive computation."""    print(f"Computing for n={n}")    time.sleep(0.5)    return n ** 2 # First calls - slowstart = time.time()results = [expensive_computation(i) for i in range(5)]print(f"First run: {time.time() - start:.2f}s")# Computing for n=0,1,2,3,4# First run: 2.50s # Second calls - instant (cached)start = time.time()results = [expensive_computation(i) for i in range(5)]print(f"Second run: {time.time() - start:.4f}s")# Second run: 0.0001s print(expensive_computation.cache_info())

functools.reduce

reduce áp dụng function lên items cumulatively.

from functools import reduce # Sum with reducenumbers = [1, 2, 3, 4, 5]total = reduce(lambda x, y: x + y, numbers)print(total)  # 15 # Productproduct = reduce(lambda x, y: x * y, numbers)print(product)  # 120 # Maximummaximum = reduce(lambda x, y: x if x > y else y, numbers)print(maximum)  # 5 # String concatenationwords = ['Hello', ' ', 'World', '!']sentence = reduce(lambda x, y: x + y, words)print(sentence)  # Hello World! # With initial valuetotal_with_init = reduce(lambda x, y: x + y, numbers, 10)print(total_with_init)  # 25 (10 + 15)

functools.wraps

wraps preserve function metadata in decorators (đã học ở Bài 2).

from functools import wraps def my_decorator(func):    @wraps(func)  # Preserve metadata    def wrapper(*args, **kwargs):        """Wrapper docstring."""        return func(*args, **kwargs)    return wrapper @my_decoratordef greet(name):    """Greet someone."""    return f"Hello, {name}!" print(greet.__name__)  # greet (not 'wrapper')print(greet.__doc__)   # Greet someone. (not 'Wrapper docstring')

functools.singledispatch

singledispatch tạo generic functions với type-based dispatch.

from functools import singledispatch @singledispatchdef process(arg):    """Default processing."""    print(f"Default: {arg}") @process.register(int)def _(arg):    """Process integer."""    print(f"Integer: {arg * 2}") @process.register(str)def _(arg):    """Process string."""    print(f"String: {arg.upper()}") @process.register(list)def _(arg):    """Process list."""    print(f"List length: {len(arg)}") # Usage - automatically dispatches based on typeprocess(42)           # Integer: 84process("hello")      # String: HELLOprocess([1, 2, 3])    # List length: 3process(3.14)         # Default: 3.14

functools.total_ordering

total_ordering tự động generate comparison methods.

from functools import total_ordering @total_orderingclass Person:    """Person with ordering."""        def __init__(self, name, age):        self.name = name        self.age = age        def __eq__(self, other):        """Equality based on age."""        if not isinstance(other, Person):            return NotImplemented        return self.age == other.age        def __lt__(self, other):        """Less than based on age."""        if not isinstance(other, Person):            return NotImplemented        return self.age < other.age        def __repr__(self):        return f"Person('{self.name}', {self.age})" # Create personsalice = Person("Alice", 25)bob = Person("Bob", 30)charlie = Person("Charlie", 25) # All comparison operators work!print(alice < bob)      # Trueprint(alice <= bob)     # Trueprint(alice == charlie) # Trueprint(alice >= bob)     # Falseprint(alice > bob)      # False # Sortpeople = [bob, alice, charlie]print(sorted(people))# [Person('Alice', 25), Person('Charlie', 25), Person('Bob', 30)]

Function Annotations

Annotations thêm metadata cho function parameters và return values.

Basic Annotations

def greet(name: str) -> str:    """Greet with type annotations."""    return f"Hello, {name}!" def add(a: int, b: int) -> int:    """Add two integers."""    return a + b def process_items(items: list, factor: float = 1.0) -> list:    """Process items with factor."""    return [item * factor for item in items] # Annotations don't enforce types!result = add("hello", "world")  # Works but wrong!print(result)  # helloworld # Access annotationsprint(greet.__annotations__)# {'name': <class 'str'>, 'return': <class 'str'>} print(add.__annotations__)# {'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}

Complex Type Annotations

from typing import List, Dict, Tuple, Optional, Union, Any def process_users(    users: List[Dict[str, Any]],    active_only: bool = True) -> List[str]:    """Process user list and return names."""    result = []    for user in users:        if not active_only or user.get('active', False):            result.append(user['name'])    return result def find_user(    user_id: int,    database: Dict[int, Dict[str, str]]) -> Optional[Dict[str, str]]:    """Find user by ID, return None if not found."""    return database.get(user_id) def parse_config(    config: Union[str, Dict[str, Any]]) -> Dict[str, Any]:    """Parse config from string or dict."""    if isinstance(config, str):        import json        return json.loads(config)    return config # Usage with proper typesusers = [    {'name': 'Alice', 'active': True},    {'name': 'Bob', 'active': False}]active_names = process_users(users)print(active_names)  # ['Alice']

Type Hints for Callables

from typing import Callable def apply_operation(    func: Callable[[int, int], int],    a: int,    b: int) -> int:    """Apply function to two integers."""    return func(a, b) def add(x: int, y: int) -> int:    return x + y def multiply(x: int, y: int) -> int:    return x * y # Usageresult1 = apply_operation(add, 5, 3)       # 8result2 = apply_operation(multiply, 5, 3)  # 15 # Higher-order function annotationdef create_multiplier(factor: int) -> Callable[[int], int]:    """Return function that multiplies by factor."""    def multiply(x: int) -> int:        return x * factor    return multiply double = create_multiplier(2)print(double(5))  # 10

Custom Types

from typing import NewType, TypeAlias # NewType - creates distinct typeUserId = NewType('UserId', int)Username = NewType('Username', str) def get_user(user_id: UserId) -> Username:    """Get username by ID."""    return Username(f"user_{user_id}") # TypeAlias - creates aliasVector: TypeAlias = List[float]Matrix: TypeAlias = List[Vector] def dot_product(v1: Vector, v2: Vector) -> float:    """Calculate dot product."""    return sum(a * b for a, b in zip(v1, v2)) # Usageuser_id = UserId(123)username = get_user(user_id) v1: Vector = [1.0, 2.0, 3.0]v2: Vector = [4.0, 5.0, 6.0]result = dot_product(v1, v2)  # 32.0

Real-world Examples

1. API Client with Partial

from functools import partialimport json def api_request(method, endpoint, base_url, headers=None, data=None):    """Make API request."""    url = f"{base_url}{endpoint}"    request = {        'method': method,        'url': url,        'headers': headers or {},        'data': data    }    print(f"API Request: {request}")    return {'status': 200, 'data': 'Success'} # Create API client for specific serviceapi = partial(    api_request,    base_url='https://api.example.com',    headers={'Authorization': 'Bearer token123'}) # Create method-specific functionsget = partial(api, 'GET')post = partial(api, 'POST')put = partial(api, 'PUT')delete = partial(api, 'DELETE') # Useget('/users')post('/users', data={'name': 'Alice'})put('/users/1', data={'name': 'Bob'})delete('/users/1')

2. Memoized Database Query

from functools import lru_cachefrom typing import Dict, List, Optional class Database:    """Database simulator."""        def __init__(self):        self.query_count = 0        @lru_cache(maxsize=128)    def get_user(self, user_id: int) -> Optional[Dict[str, any]]:        """Get user by ID (cached)."""        self.query_count += 1        print(f"Query #{self.query_count}: SELECT * FROM users WHERE id={user_id}")                # Simulate database query        users = {            1: {'id': 1, 'name': 'Alice'},            2: {'id': 2, 'name': 'Bob'}        }        return users.get(user_id)        @lru_cache(maxsize=64)    def get_posts_by_user(self, user_id: int) -> List[Dict[str, any]]:        """Get posts by user (cached)."""        self.query_count += 1        print(f"Query #{self.query_count}: SELECT * FROM posts WHERE user_id={user_id}")                # Simulate database query        return [            {'id': 1, 'user_id': user_id, 'title': 'Post 1'},            {'id': 2, 'user_id': user_id, 'title': 'Post 2'}        ] # Usagedb = Database() # First calls - execute queriesuser1 = db.get_user(1)user2 = db.get_user(1)  # Cached!posts = db.get_posts_by_user(1) print(f"\nTotal queries: {db.query_count}")  # 2 (not 3) # Cache statsprint(db.get_user.cache_info())

3. Decorator with Arguments

from functools import wrapsfrom typing import Callable, Any def validate_args(**validators):    """Validate function arguments."""    def decorator(func: Callable) -> Callable:        @wraps(func)        def wrapper(*args, **kwargs):            # Get function signature            import inspect            sig = inspect.signature(func)            bound = sig.bind(*args, **kwargs)                        # Validate            for param_name, validator in validators.items():                if param_name in bound.arguments:                    value = bound.arguments[param_name]                    if not validator(value):                        raise ValueError(                            f"Invalid value for {param_name}: {value}"                        )                        return func(*args, **kwargs)        return wrapper    return decorator # Usage@validate_args(    age=lambda x: 0 <= x <= 120,    email=lambda x: '@' in x)def create_user(name: str, age: int, email: str) -> Dict[str, Any]:    """Create user with validation."""    return {'name': name, 'age': age, 'email': email} # Validuser = create_user('Alice', 25, '[email protected]')print(user) # Invalid - raises ValueErrortry:    create_user('Bob', 150, '[email protected]')except ValueError as e:    print(f"Error: {e}")

4. Function Pipeline

from functools import reducefrom typing import Callable, TypeVar, List T = TypeVar('T') def pipe(*functions: Callable) -> Callable:    """Create function pipeline."""    def pipeline(initial_value: T) -> T:        return reduce(lambda value, func: func(value), functions, initial_value)    return pipeline # Create processing functionsdef remove_whitespace(text: str) -> str:    return text.strip() def to_lowercase(text: str) -> str:    return text.lower() def replace_spaces(text: str) -> str:    return text.replace(' ', '_') def add_prefix(text: str) -> str:    return f"processed_{text}" # Build pipelineprocess_text = pipe(    remove_whitespace,    to_lowercase,    replace_spaces,    add_prefix) # Use pipelineresult = process_text("  Hello World  ")print(result)  # processed_hello_world # Another pipelinedef multiply_by_2(x: int) -> int:    return x * 2 def add_10(x: int) -> int:    return x + 10 def square(x: int) -> int:    return x ** 2 process_number = pipe(multiply_by_2, add_10, square)print(process_number(5))  # ((5*2)+10)^2 = 400

5. Generic Repository Pattern

from functools import lru_cachefrom typing import TypeVar, Generic, List, Optional, Callablefrom dataclasses import dataclass T = TypeVar('T') @dataclassclass User:    id: int    name: str    email: str class Repository(Generic[T]):    """Generic repository with caching."""        def __init__(self, data_source: Callable[[], List[T]]):        self.data_source = data_source        @lru_cache(maxsize=128)    def get_by_id(self, id: int) -> Optional[T]:        """Get entity by ID (cached)."""        items = self.data_source()        for item in items:            if hasattr(item, 'id') and item.id == id:                return item        return None        @lru_cache(maxsize=64)    def get_all(self) -> List[T]:        """Get all entities (cached)."""        return self.data_source()        def clear_cache(self):        """Clear all caches."""        self.get_by_id.cache_clear()        self.get_all.cache_clear() # Create data sourcesdef get_users() -> List[User]:    """Simulate database query."""    print("Loading users from database...")    return [        User(1, 'Alice', '[email protected]'),        User(2, 'Bob', '[email protected]')    ] # Create repositoryuser_repo = Repository[User](get_users) # Use repositoryuser = user_repo.get_by_id(1)  # Loads from databaseprint(user) user = user_repo.get_by_id(1)  # Uses cacheprint(user) all_users = user_repo.get_all()  # Uses cache (already loaded)print(len(all_users))

Best Practices

# 1. Use type hints for claritydef process_data(data: List[int]) -> int:    """Clear types improve readability."""    return sum(data) # 2. Use partial for configurationfrom functools import partial def connect(host, port, timeout):    pass connect_prod = partial(connect, host='prod.example.com', port=5432)# Now just call: connect_prod(timeout=30) # 3. Use lru_cache for expensive computationsfrom functools import lru_cache @lru_cache(maxsize=128)def expensive_function(n):    # Expensive computation    pass # 4. Preserve metadata with @wrapsfrom functools import wraps def decorator(func):    @wraps(func)  # Always use this!    def wrapper(*args, **kwargs):        return func(*args, **kwargs)    return wrapper # 5. Use *args/**kwargs for flexible APIsdef flexible_api(*args, **kwargs):    """Accept any arguments for flexibility."""    pass

Bài Tập Thực Hành

Bài 1: Retry with Exponential Backoff

Tạo decorator retry với exponential backoff using partial.

Bài 2: Query Builder

Tạo database query builder using partial và closures.

Bài 3: Compose Function

Tạo compose() function kết hợp nhiều functions.

Bài 4: Typed Cache

Tạo cache decorator với type validation.

Bài 5: Command Pattern

Implement command pattern with type hints và functools.

Tóm Tắt

✅ *args: Variable positional arguments (tuple)
✅ **kwargs: Variable keyword arguments (dict)
functools.partial: Pre-fill function arguments
functools.lru_cache: Memoize function results
functools.reduce: Cumulative operations
functools.singledispatch: Type-based dispatch
Type hints: Annotations for types
typing module: Complex type annotations

Kết Luận Cả 2 Parts

Part 1 - First-Class Functions & Closures:

  • First-class functions là objects
  • Nested functions và closures
  • Function factories
  • Closure patterns

**Part 2 - *args, kwargs & functools:

  • Variable-length arguments
  • functools utilities
  • Function annotations
  • Type hints

Key Takeaways:

  • Functions are powerful in Python
  • Use closures for encapsulation
  • functools provides useful utilities
  • Type hints improve code quality
  • Partial functions enable configuration

Bài Tiếp Theo

Bài 6: Comprehensions - List, dict, set comprehensions, và nested comprehensions! 🚀


Remember:

  • *args for variable positional args
  • **kwargs for variable keyword args
  • Use functools utilities
  • Type hints document intent
  • Keep functions focused! 🎯