Bài 2: Decorators - Function Decorators
Mục Tiêu Bài Học
Sau khi hoàn thành bài này, bạn sẽ:
- ✅ Hiểu decorators là gì và cách hoạt động
- ✅ Tạo basic function decorators
- ✅ Sử dụng functools.wraps
- ✅ Tạo decorators với arguments
- ✅ Stack multiple decorators
- ✅ Áp dụng common decorator patterns
Decorators Là Gì?
Decorator là function nhận function làm input và return function mới.
Concept
# Without decoratordef greet(): return "Hello!" def make_bold(func): def wrapper(): return f"<b>{func()}</b>" return wrapper greet = make_bold(greet)print(greet()) # <b>Hello!</b> # With decorator syntax (syntactic sugar)@make_bolddef greet(): return "Hello!" print(greet()) # <b>Hello!</b>
Basic Decorator Structure
def my_decorator(func): """ Basic decorator template. Args: func: Function to decorate Returns: wrapper: New function that wraps original """ def wrapper(*args, **kwargs): # Code before function call print("Before function") # Call original function result = func(*args, **kwargs) # Code after function call print("After function") return result return wrapper @my_decoratordef say_hello(name): print(f"Hello, {name}!") return "Done" # Usageresult = say_hello("Alice")# Output:# Before function# Hello, Alice!# After function print(result) # Done
Basic Decorators
1. Timer Decorator
import timefrom functools import wraps def timer(func): """Measure function execution time.""" @wraps(func) def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(f"{func.__name__} took {end - start:.4f} seconds") return result return wrapper @timerdef slow_function(): """Simulate slow operation.""" time.sleep(1) return "Done" @timerdef calculate_sum(n): """Calculate sum of numbers.""" return sum(range(n)) # Usageslow_function() # slow_function took 1.0001 secondsresult = calculate_sum(1000000) # calculate_sum took 0.0234 seconds
2. Logger Decorator
from functools import wrapsfrom datetime import datetime def logger(func): """Log function calls with arguments.""" @wraps(func) def wrapper(*args, **kwargs): timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') # Log call args_repr = [repr(a) for a in args] kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()] signature = ", ".join(args_repr + kwargs_repr) print(f"[{timestamp}] Calling {func.__name__}({signature})") # Execute function result = func(*args, **kwargs) # Log result print(f"[{timestamp}] {func.__name__} returned {result!r}") return result return wrapper @loggerdef add(a, b): """Add two numbers.""" return a + b @loggerdef greet(name, greeting="Hello"): """Greet someone.""" return f"{greeting}, {name}!" # Usageadd(5, 3)# [2025-10-27 10:30:00] Calling add(5, 3)# [2025-10-27 10:30:00] add returned 8 greet("Alice", greeting="Hi")# [2025-10-27 10:30:00] Calling greet('Alice', greeting='Hi')# [2025-10-27 10:30:00] greet returned 'Hi, Alice!'
3. Cache Decorator
from functools import wraps def cache(func): """Simple cache decorator.""" cached_results = {} @wraps(func) def wrapper(*args): if args not in cached_results: print(f"Computing {func.__name__}{args}...") cached_results[args] = func(*args) else: print(f"Using cached result for {func.__name__}{args}") return cached_results[args] return wrapper @cachedef fibonacci(n): """Calculate fibonacci number.""" if n < 2: return n return fibonacci(n - 1) + fibonacci(n - 2) @cachedef expensive_computation(x, y): """Simulate expensive computation.""" import time time.sleep(1) return x ** y # Usageprint(fibonacci(10)) # Computing...print(fibonacci(10)) # Using cached result print(expensive_computation(2, 10)) # Takes 1 secondprint(expensive_computation(2, 10)) # Instant (cached)
functools.wraps
Problem: Decorators thay đổi metadata của function.
def my_decorator(func): def wrapper(*args, **kwargs): """Wrapper docstring.""" return func(*args, **kwargs) return wrapper @my_decoratordef greet(name): """Greet someone by name.""" return f"Hello, {name}!" # Metadata is lost!print(greet.__name__) # wrapper (should be 'greet')print(greet.__doc__) # Wrapper docstring (should be 'Greet someone...')
Solution: Use functools.wraps
from functools import wraps def my_decorator(func): @wraps(func) # Preserves metadata! def wrapper(*args, **kwargs): """Wrapper docstring.""" return func(*args, **kwargs) return wrapper @my_decoratordef greet(name): """Greet someone by name.""" return f"Hello, {name}!" # Metadata preserved!print(greet.__name__) # greetprint(greet.__doc__) # Greet someone by name.
Decorators với Arguments
Pattern 1: Decorator Factory
from functools import wraps def repeat(times): """ Decorator factory - returns decorator. Args: times: Number of times to repeat function call """ def decorator(func): @wraps(func) def wrapper(*args, **kwargs): results = [] for _ in range(times): result = func(*args, **kwargs) results.append(result) return results return wrapper return decorator @repeat(3)def greet(name): return f"Hello, {name}!" # Usageresults = greet("Alice")print(results)# ['Hello, Alice!', 'Hello, Alice!', 'Hello, Alice!']
Pattern 2: Flexible Decorator
from functools import wraps def smart_logger(prefix="LOG"): """Logger with configurable prefix.""" def decorator(func): @wraps(func) def wrapper(*args, **kwargs): print(f"[{prefix}] Calling {func.__name__}") result = func(*args, **kwargs) print(f"[{prefix}] {func.__name__} returned {result}") return result return wrapper return decorator @smart_logger(prefix="DEBUG")def add(a, b): return a + b @smart_logger(prefix="INFO")def multiply(a, b): return a * b # Usageadd(5, 3)# [DEBUG] Calling add# [DEBUG] add returned 8 multiply(4, 5)# [INFO] Calling multiply# [INFO] multiply returned 20
Pattern 3: Optional Arguments
from functools import wraps def validate(func=None, *, min_value=0, max_value=100): """ Validate function arguments. Can be used with or without arguments. """ def decorator(f): @wraps(f) def wrapper(*args, **kwargs): # Validate first argument if args: value = args[0] if not min_value <= value <= max_value: raise ValueError( f"Value must be between {min_value} and {max_value}" ) return f(*args, **kwargs) return wrapper # Allow usage without parentheses if func is not None: return decorator(func) return decorator # Usage without arguments@validatedef process(value): return value * 2 # Usage with arguments@validate(min_value=10, max_value=50)def calculate(value): return value ** 2 # Testprint(process(50)) # 100# process(150) # ValueError print(calculate(20)) # 400# calculate(5) # ValueError
Stacking Decorators
from functools import wrapsimport time def timer(func): @wraps(func) def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(f"Time: {end - start:.4f}s") return result return wrapper def logger(func): @wraps(func) def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") result = func(*args, **kwargs) print(f"Result: {result}") return result return wrapper def uppercase(func): @wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) if isinstance(result, str): return result.upper() return result return wrapper # Stack multiple decorators# Applied bottom to top: uppercase -> logger -> timer@timer@logger@uppercasedef greet(name): return f"hello, {name}" # Usageresult = greet("alice")# Output:# Calling greet# Result: HELLO, ALICE# Time: 0.0001s print(result) # HELLO, ALICE
Practical Decorator Patterns
1. Retry Decorator
from functools import wrapsimport time def retry(max_attempts=3, delay=1): """Retry function on exception.""" def decorator(func): @wraps(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}") print(f"Retrying in {delay} seconds...") time.sleep(delay) return wrapper return decorator @retry(max_attempts=3, delay=2)def unreliable_api_call(): """Simulate unreliable API.""" import random if random.random() < 0.7: # 70% chance of failure raise ConnectionError("API unavailable") return "Success!" # Usageresult = unreliable_api_call()# May retry several times before success
2. Rate Limiter
from functools import wrapsimport time def rate_limit(max_calls, period): """ Limit function calls per period. Args: max_calls: Maximum number of calls period: Time period in seconds """ calls = [] def decorator(func): @wraps(func) def wrapper(*args, **kwargs): now = time.time() # Remove old calls outside period while calls and calls[0] < now - period: calls.pop(0) # Check limit if len(calls) >= max_calls: wait_time = period - (now - calls[0]) raise RuntimeError( f"Rate limit exceeded. Wait {wait_time:.1f}s" ) # Record call and execute calls.append(now) return func(*args, **kwargs) return wrapper return decorator @rate_limit(max_calls=3, period=10)def api_call(endpoint): """Simulate API call.""" print(f"Calling {endpoint}") return f"Response from {endpoint}" # Usagefor i in range(5): try: api_call(f"/api/endpoint{i}") time.sleep(2) except RuntimeError as e: print(f"Error: {e}")
3. Validation Decorator
from functools import wraps def validate_types(**type_hints): """Validate function argument types.""" def decorator(func): @wraps(func) def wrapper(*args, **kwargs): # Get function signature import inspect sig = inspect.signature(func) bound = sig.bind(*args, **kwargs) # Validate types for param_name, param_value in bound.arguments.items(): if param_name in type_hints: expected_type = type_hints[param_name] if not isinstance(param_value, expected_type): raise TypeError( f"{param_name} must be {expected_type.__name__}, " f"got {type(param_value).__name__}" ) return func(*args, **kwargs) return wrapper return decorator @validate_types(name=str, age=int, salary=float)def create_user(name, age, salary): """Create user with validated types.""" return { 'name': name, 'age': age, 'salary': salary } # Usageuser = create_user("Alice", 25, 50000.0)print(user) # {'name': 'Alice', 'age': 25, 'salary': 50000.0} # Type validation# create_user("Bob", "25", 60000.0) # TypeError: age must be int# create_user("Charlie", 30, 70000) # TypeError: salary must be float
4. Deprecation Warning
from functools import wrapsimport warnings def deprecated(reason): """Mark function as deprecated.""" def decorator(func): @wraps(func) def wrapper(*args, **kwargs): warnings.warn( f"{func.__name__} is deprecated. {reason}", category=DeprecationWarning, stacklevel=2 ) return func(*args, **kwargs) return wrapper return decorator @deprecated("Use new_function() instead")def old_function(x): """Old function - deprecated.""" return x * 2 @deprecated("This API will be removed in v2.0")def legacy_api(): """Legacy API endpoint.""" return "Legacy response" # Usageresult = old_function(5)# DeprecationWarning: old_function is deprecated. Use new_function() instead
5. Permission Check
from functools import wraps class User: """Simple user class.""" def __init__(self, name, role): self.name = name self.role = role # Global current user (in real app, would be from session)current_user = None def require_role(*allowed_roles): """Check if user has required role.""" def decorator(func): @wraps(func) def wrapper(*args, **kwargs): if current_user is None: raise PermissionError("Not authenticated") if current_user.role not in allowed_roles: raise PermissionError( f"Required role: {allowed_roles}, " f"got: {current_user.role}" ) return func(*args, **kwargs) return wrapper return decorator @require_role('admin')def delete_user(user_id): """Delete user - admin only.""" return f"User {user_id} deleted" @require_role('admin', 'moderator')def edit_content(content_id): """Edit content - admin or moderator.""" return f"Content {content_id} edited" @require_role('admin', 'moderator', 'user')def view_content(content_id): """View content - any authenticated user.""" return f"Viewing content {content_id}" # Usagecurrent_user = User("Alice", "admin")print(delete_user(123)) # User 123 deleted current_user = User("Bob", "moderator")print(edit_content(456)) # Content 456 edited# delete_user(789) # PermissionError current_user = User("Charlie", "user")print(view_content(999)) # Viewing content 999# delete_user(111) # PermissionError
Ví Dụ Thực Tế
1. Web Framework Decorators
from functools import wraps class SimpleFramework: """Simple web framework.""" def __init__(self): self.routes = {} def route(self, path, methods=None): """Register route.""" if methods is None: methods = ['GET'] def decorator(func): self.routes[path] = { 'handler': func, 'methods': methods } return func return decorator def handle_request(self, path, method='GET'): """Handle HTTP request.""" if path not in self.routes: return "404 Not Found" route = self.routes[path] if method not in route['methods']: return "405 Method Not Allowed" return route['handler']() # Create appapp = SimpleFramework() @app.route('/')def home(): return "Welcome to homepage!" @app.route('/about')def about(): return "About us page" @app.route('/api/users', methods=['GET', 'POST'])def users(): return "Users API endpoint" # Usageprint(app.handle_request('/')) # Welcome to homepage!print(app.handle_request('/about')) # About us pageprint(app.handle_request('/api/users')) # Users API endpointprint(app.handle_request('/missing')) # 404 Not Found
2. Database Transaction
from functools import wraps class Database: """Simple database simulator.""" def __init__(self): self.data = {} self.in_transaction = False self.transaction_data = {} def begin_transaction(self): """Start transaction.""" self.in_transaction = True self.transaction_data = self.data.copy() def commit(self): """Commit transaction.""" self.in_transaction = False self.transaction_data = {} def rollback(self): """Rollback transaction.""" self.data = self.transaction_data.copy() self.in_transaction = False self.transaction_data = {} def set(self, key, value): """Set value.""" self.data[key] = value def get(self, key): """Get value.""" return self.data.get(key) def transactional(func): """Execute function in transaction.""" @wraps(func) def wrapper(db, *args, **kwargs): db.begin_transaction() try: result = func(db, *args, **kwargs) db.commit() print("Transaction committed") return result except Exception as e: db.rollback() print(f"Transaction rolled back: {e}") raise return wrapper @transactionaldef transfer_money(db, from_user, to_user, amount): """Transfer money between users.""" # Get balances from_balance = db.get(from_user) or 0 to_balance = db.get(to_user) or 0 # Validate if from_balance < amount: raise ValueError("Insufficient funds") # Perform transfer db.set(from_user, from_balance - amount) db.set(to_user, to_balance + amount) return f"Transferred ${amount} from {from_user} to {to_user}" # Usagedb = Database()db.set('alice', 1000)db.set('bob', 500) # Successful transferresult = transfer_money(db, 'alice', 'bob', 200)print(result)print(f"Alice: ${db.get('alice')}, Bob: ${db.get('bob')}")# Transaction committed# Transferred $200 from alice to bob# Alice: $800, Bob: $700 # Failed transfer (rollback)try: transfer_money(db, 'alice', 'bob', 10000)except ValueError: pass print(f"Alice: ${db.get('alice')}, Bob: ${db.get('bob')}")# Transaction rolled back: Insufficient funds# Alice: $800, Bob: $700 (unchanged)
Best Practices
# 1. Always use @wrapsfrom functools import wraps def my_decorator(func): @wraps(func) # Preserves metadata def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper # 2. Use *args, **kwargs for flexibilitydef flexible_decorator(func): @wraps(func) def wrapper(*args, **kwargs): # Accepts any arguments return func(*args, **kwargs) return wrapper # 3. Document decoratorsdef documented_decorator(func): """ This decorator does X. Args: func: Function to decorate Returns: wrapper: Decorated function """ @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper # 4. Keep decorators simple and focuseddef single_purpose(func): """Do one thing well.""" @wraps(func) def wrapper(*args, **kwargs): # Single, clear purpose return func(*args, **kwargs) return wrapper
Bài Tập Thực Hành
Bài 1: Memoization Decorator
Tạo decorator cache kết quả với TTL (time-to-live).
Bài 2: Timeout Decorator
Tạo decorator giới hạn thời gian thực thi function.
Bài 3: Debug Decorator
Tạo decorator in ra arguments, return value, và exception nếu có.
Bài 4: Counter Decorator
Tạo decorator đếm số lần function được gọi.
Bài 5: Conditional Decorator
Tạo decorator chỉ chạy function nếu điều kiện thỏa mãn.
Tóm Tắt
✅ Decorator: Function nhận function, return function
✅ @wraps: Preserve function metadata
✅ Decorator factory: Function return decorator
✅ Stacking: Multiple decorators áp dụng bottom-to-top
✅ Common patterns: Timer, logger, cache, retry, validation
✅ Real-world: Routes, transactions, permissions
Bài Tiếp Theo
Bài 2.2: Decorators - Class decorators, property decorators, decorator classes, và advanced patterns!
Remember:
- Decorators = Wrapper functions
- Always use @wraps
- Keep decorators simple
- *args, **kwargs for flexibility
- Test decorated functions! 🎯