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