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