Skip to content

Decorators Deep Dive Nâng cao

Decorators = Hàm bọc hàm = Siêu năng lực của Python

Learning Outcomes

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

  • ✅ Hiểu sâu cơ chế hoạt động của decorators
  • ✅ Viết được decorator factories với arguments
  • ✅ Sử dụng class decorators cho stateful logic
  • ✅ Hiểu tầm quan trọng của functools.wraps
  • ✅ Nắm vững thứ tự thực thi khi stacking decorators

Decorator là gì?

Decorator là một hàm nhận một hàm khác làm đầu vào và trả về một hàm mới với chức năng được mở rộng — mà không thay đổi code gốc.

Ví dụ Đơn giản

python
def decorator(func):
    """Decorator đơn giản."""
    def wrapper(*args, **kwargs):
        print("Trước khi gọi hàm")
        ket_qua = func(*args, **kwargs)
        print("Sau khi gọi hàm")
        return ket_qua
    return wrapper

@decorator
def chao():
    print("Xin chào!")

chao()
# Output:
# Trước khi gọi hàm
# Xin chào!
# Sau khi gọi hàm

Cú pháp @ là gì?

python
@decorator
def chao():
    pass

# Tương đương với:
def chao():
    pass
chao = decorator(chao)

Tầm Quan Trọng của functools.wraps 🔑

Vấn đề: Mất Metadata

Khi bọc một hàm, wrapper function sẽ thay thế metadata của hàm gốc:

python
def my_decorator(func):
    def wrapper(*args, **kwargs):
        """Wrapper docstring."""
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def greet(name: str) -> str:
    """Chào người dùng theo tên."""
    return f"Xin chào, {name}!"

# ❌ Metadata bị mất!
print(greet.__name__)      # 'wrapper' - không phải 'greet'
print(greet.__doc__)       # 'Wrapper docstring.' - không phải docstring gốc
print(greet.__annotations__)  # {} - mất type hints!

Giải pháp: @wraps

python
from functools import wraps

def my_decorator(func):
    @wraps(func)  # ✅ Giữ nguyên metadata
    def wrapper(*args, **kwargs):
        """Wrapper docstring."""
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def greet(name: str) -> str:
    """Chào người dùng theo tên."""
    return f"Xin chào, {name}!"

# ✅ Metadata được bảo toàn!
print(greet.__name__)      # 'greet'
print(greet.__doc__)       # 'Chào người dùng theo tên.'
print(greet.__annotations__)  # {'name': <class 'str'>, 'return': <class 'str'>}

@wraps Bảo Toàn Những Gì?

AttributeMô tả
__name__Tên hàm
__doc__Docstring
__annotations__Type hints
__module__Module chứa hàm
__qualname__Qualified name (cho nested functions)
__dict__Attributes của hàm
__wrapped__Reference đến hàm gốc

Truy cập Hàm Gốc qua __wrapped__

python
from functools import wraps

def logged(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@logged
def add(a: int, b: int) -> int:
    return a + b

# Gọi hàm gốc (bypass decorator)
original_add = add.__wrapped__
print(original_add(2, 3))  # 5 - không có log

⚠️ PRODUCTION PITFALL

Không dùng @wraps = Debug nightmare!

Khi có lỗi, stack trace sẽ hiển thị wrapper thay vì tên hàm thực. Trong production với hàng trăm decorated functions, việc tìm lỗi sẽ cực kỳ khó khăn.


Decorator Factories (Decorators với Arguments)

Cấu trúc 3 Lớp

Khi decorator cần nhận arguments, bạn cần thêm một lớp bọc ngoài:

python
from functools import wraps
from typing import Callable, Any

def repeat(times: int = 2):
    """Decorator factory: lặp lại hàm n lần."""
    def decorator(func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def say_hello():
    print("Hello!")

say_hello()
# Output:
# Hello!
# Hello!
# Hello!

Decorator Factory với Default Arguments

Pattern phổ biến: decorator có thể dùng có hoặc không có parentheses:

python
from functools import wraps
from typing import Callable, TypeVar, overload

F = TypeVar('F', bound=Callable)

@overload
def debug(func: F) -> F: ...
@overload
def debug(*, prefix: str = "") -> Callable[[F], F]: ...

def debug(func: Callable | None = None, *, prefix: str = ""):
    """
    Decorator linh hoạt - dùng được cả 2 cách:
    @debug
    @debug(prefix="[API]")
    """
    def decorator(fn: Callable) -> Callable:
        @wraps(fn)
        def wrapper(*args, **kwargs):
            print(f"{prefix}Calling {fn.__name__}")
            result = fn(*args, **kwargs)
            print(f"{prefix}{fn.__name__} returned {result}")
            return result
        return wrapper
    
    if func is not None:
        # Gọi không có parentheses: @debug
        return decorator(func)
    # Gọi có parentheses: @debug(prefix="...")
    return decorator

# Cả 2 cách đều hoạt động:
@debug
def add(a, b):
    return a + b

@debug(prefix="[MATH] ")
def multiply(a, b):
    return a * b

Retry Decorator với Exponential Backoff

python
import time
from functools import wraps
from typing import Callable, Type

def retry(
    max_attempts: int = 3,
    delay: float = 1.0,
    backoff: float = 2.0,
    exceptions: tuple[Type[Exception], ...] = (Exception,)
):
    """
    Retry decorator với exponential backoff.
    
    Args:
        max_attempts: Số lần thử tối đa
        delay: Delay ban đầu (giây)
        backoff: Hệ số nhân delay sau mỗi lần thất bại
        exceptions: Tuple các exception types cần retry
    """
    def decorator(func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args, **kwargs):
            current_delay = delay
            last_exception = None
            
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    last_exception = e
                    if attempt < max_attempts:
                        print(f"Attempt {attempt} failed: {e}. "
                              f"Retrying in {current_delay:.1f}s...")
                        time.sleep(current_delay)
                        current_delay *= backoff
            
            raise last_exception
        return wrapper
    return decorator

@retry(max_attempts=5, delay=0.5, backoff=2.0, exceptions=(ConnectionError, TimeoutError))
def fetch_data(url: str) -> dict:
    """Fetch data từ API không ổn định."""
    import random
    if random.random() < 0.7:
        raise ConnectionError("Connection refused")
    return {"status": "success"}

Class Decorators 🏛️

Class as Decorator (Stateful Decorators)

Khi decorator cần lưu trữ state, class là lựa chọn tốt hơn function:

python
from functools import update_wrapper
from typing import Callable, Any

class CountCalls:
    """Đếm số lần hàm được gọi."""
    
    def __init__(self, func: Callable):
        self.func = func
        self.count = 0
        # Tương đương @wraps cho class decorator
        update_wrapper(self, func)
    
    def __call__(self, *args: Any, **kwargs: Any) -> Any:
        self.count += 1
        print(f"Call #{self.count} to {self.func.__name__}")
        return self.func(*args, **kwargs)
    
    def reset(self) -> None:
        """Reset counter về 0."""
        self.count = 0

@CountCalls
def process_item(item: str) -> str:
    return f"Processed: {item}"

process_item("A")  # Call #1 to process_item
process_item("B")  # Call #2 to process_item
print(process_item.count)  # 2
process_item.reset()
print(process_item.count)  # 0
)

Class Decorator Factory

python
from functools import update_wrapper
from typing import Callable, Any
import time

class RateLimiter:
    """Rate limiter decorator với configurable limit."""
    
    def __init__(self, calls_per_second: float = 1.0):
        self.min_interval = 1.0 / calls_per_second
        self.last_call = 0.0
        self.func: Callable | None = None
    
    def __call__(self, func: Callable) -> "RateLimiter":
        self.func = func
        update_wrapper(self, func)
        return self
    
    def __get__(self, obj, objtype=None):
        """Support instance methods."""
        if obj is None:
            return self
        return lambda *args, **kwargs: self._call(obj, *args, **kwargs)
    
    def _call(self, *args: Any, **kwargs: Any) -> Any:
        elapsed = time.time() - self.last_call
        if elapsed < self.min_interval:
            time.sleep(self.min_interval - elapsed)
        
        self.last_call = time.time()
        return self.func(*args, **kwargs)

@RateLimiter(calls_per_second=2)
def call_api(endpoint: str) -> str:
    return f"Called {endpoint}"

# Mỗi call cách nhau ít nhất 0.5s
for i in range(5):
    print(call_api(f"/api/item/{i}"))

Decorating Classes (Class Decorator Pattern)

Decorator không chỉ dùng cho functions — còn có thể decorate classes:

python
from typing import Type, TypeVar
from dataclasses import dataclass, field
from datetime import datetime

T = TypeVar('T')

def add_timestamps(cls: Type[T]) -> Type[T]:
    """Thêm created_at và updated_at vào class."""
    original_init = cls.__init__
    
    def new_init(self, *args, **kwargs):
        original_init(self, *args, **kwargs)
        self.created_at = datetime.now()
        self.updated_at = datetime.now()
    
    def touch(self):
        """Update timestamp."""
        self.updated_at = datetime.now()
    
    cls.__init__ = new_init
    cls.touch = touch
    return cls

@add_timestamps
class User:
    def __init__(self, name: str, email: str):
        self.name = name
        self.email = email

user = User("HPN", "hpn@example.com")
print(user.created_at)  # 2024-01-15 10:30:00
user.touch()
print(user.updated_at)  # 2024-01-15 10:30:05

Singleton Pattern với Class Decorator

python
from typing import Type, TypeVar, Any
from functools import wraps

T = TypeVar('T')

def singleton(cls: Type[T]) -> Type[T]:
    """Biến class thành singleton."""
    instances: dict[Type, Any] = {}
    
    @wraps(cls, updated=[])
    class SingletonWrapper(cls):
        def __new__(cls_inner, *args, **kwargs):
            if cls not in instances:
                instances[cls] = super().__new__(cls_inner)
            return instances[cls]
    
    return SingletonWrapper

@singleton
class DatabaseConnection:
    def __init__(self, host: str = "localhost"):
        self.host = host
        print(f"Connecting to {host}...")

# Chỉ tạo 1 instance duy nhất
db1 = DatabaseConnection("server1")  # Connecting to server1...
db2 = DatabaseConnection("server2")  # Không in gì - dùng instance cũ
print(db1 is db2)  # True

Registry Pattern

python
from typing import Callable, Type

class PluginRegistry:
    """Registry pattern cho plugin system."""
    
    def __init__(self):
        self._plugins: dict[str, Type] = {}
    
    def register(self, name: str | None = None):
        """Decorator để đăng ký plugin."""
        def decorator(cls: Type) -> Type:
            plugin_name = name or cls.__name__
            self._plugins[plugin_name] = cls
            return cls
        return decorator
    
    def get(self, name: str) -> Type | None:
        return self._plugins.get(name)
    
    def list_plugins(self) -> list[str]:
        return list(self._plugins.keys())

# Sử dụng
plugins = PluginRegistry()

@plugins.register("json")
class JsonParser:
    def parse(self, data: str) -> dict:
        import json
        return json.loads(data)

@plugins.register("xml")
class XmlParser:
    def parse(self, data: str) -> dict:
        # XML parsing logic
        return {}

print(plugins.list_plugins())  # ['json', 'xml']
parser_cls = plugins.get("json")
parser = parser_cls()

Decorator Stacking Order 📚

Thứ tự Áp dụng vs Thứ tự Thực thi

python
@decorator_a
@decorator_b
@decorator_c
def func():
    pass

# Tương đương với:
func = decorator_a(decorator_b(decorator_c(func)))

Quy tắc:

  • Áp dụng: Từ dưới lên (gần hàm nhất → xa nhất)
  • Thực thi: Từ trên xuống (xa nhất → gần nhất)

Ví dụ Minh họa

python
from functools import wraps

def decorator_a(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("A: before")
        result = func(*args, **kwargs)
        print("A: after")
        return result
    return wrapper

def decorator_b(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("B: before")
        result = func(*args, **kwargs)
        print("B: after")
        return result
    return wrapper

def decorator_c(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("C: before")
        result = func(*args, **kwargs)
        print("C: after")
        return result
    return wrapper

@decorator_a
@decorator_b
@decorator_c
def greet():
    print("Hello!")

greet()
# Output:
# A: before
# B: before
# C: before
# Hello!
# C: after
# B: after
# A: after

Visualizing the Call Stack

Thứ tự Quan trọng trong Production

python
from functools import wraps
import time

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"⏱️ {func.__name__}: {elapsed:.4f}s")
        return result
    return wrapper

def retry(max_attempts: int = 3):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts - 1:
                        raise
                    print(f"Retry {attempt + 1}...")
        return wrapper
    return decorator

def cache(func):
    memo = {}
    @wraps(func)
    def wrapper(*args):
        if args not in memo:
            memo[args] = func(*args)
        return memo[args]
    return wrapper

# ✅ Đúng thứ tự: timer đo tổng thời gian (bao gồm retries)
@timer
@retry(max_attempts=3)
@cache
def fetch_data(url: str) -> dict:
    ...

# ❌ Sai thứ tự: timer chỉ đo 1 attempt
@retry(max_attempts=3)
@timer
@cache
def fetch_data_wrong(url: str) -> dict:
    ...

💡 BEST PRACTICE: Thứ tự Decorator Phổ biến

python
@route("/api/users")      # 1. Routing (outermost)
@authenticate             # 2. Authentication
@authorize("admin")       # 3. Authorization
@rate_limit(100)          # 4. Rate limiting
@cache(ttl=300)           # 5. Caching
@timer                    # 6. Timing
@retry(max_attempts=3)    # 7. Retry (innermost)
def get_users():
    ...

Decorator Thực tế trong Production

1. Validation Decorator

python
from functools import wraps
from typing import Callable, get_type_hints

def validate_types(func: Callable) -> Callable:
    """Runtime type checking decorator."""
    hints = get_type_hints(func)
    
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Validate positional args
        arg_names = func.__code__.co_varnames[:len(args)]
        for name, value in zip(arg_names, args):
            if name in hints:
                expected = hints[name]
                if not isinstance(value, expected):
                    raise TypeError(
                        f"Argument '{name}' must be {expected.__name__}, "
                        f"got {type(value).__name__}"
                    )
        
        # Validate keyword args
        for name, value in kwargs.items():
            if name in hints:
                expected = hints[name]
                if not isinstance(value, expected):
                    raise TypeError(
                        f"Argument '{name}' must be {expected.__name__}, "
                        f"got {type(value).__name__}"
                    )
        
        return func(*args, **kwargs)
    return wrapper

@validate_types
def create_user(name: str, age: int, active: bool = True) -> dict:
    return {"name": name, "age": age, "active": active}

create_user("HPN", 25)           # ✅ OK
create_user("HPN", "twenty-five") # ❌ TypeError

2. Deprecation Warning

python
import warnings
from functools import wraps
from typing import Callable

def deprecated(reason: str = "", version: str = ""):
    """Mark function as deprecated."""
    def decorator(func: Callable) -> Callable:
        message = f"{func.__name__} is deprecated"
        if version:
            message += f" since version {version}"
        if reason:
            message += f". {reason}"
        
        @wraps(func)
        def wrapper(*args, **kwargs):
            warnings.warn(message, DeprecationWarning, stacklevel=2)
            return func(*args, **kwargs)
        return wrapper
    return decorator

@deprecated(reason="Use new_function() instead", version="2.0")
def old_function():
    return "old result"

old_function()  # DeprecationWarning: old_function is deprecated...

3. Context-Aware Decorator

python
from functools import wraps
from typing import Callable
from contextvars import ContextVar

# Context variable cho request ID
request_id: ContextVar[str] = ContextVar('request_id', default='unknown')

def with_request_context(func: Callable) -> Callable:
    """Inject request context vào logs."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        rid = request_id.get()
        print(f"[{rid}] Entering {func.__name__}")
        try:
            result = func(*args, **kwargs)
            print(f"[{rid}] {func.__name__} completed successfully")
            return result
        except Exception as e:
            print(f"[{rid}] {func.__name__} failed: {e}")
            raise
    return wrapper

@with_request_context
def process_order(order_id: int) -> dict:
    return {"order_id": order_id, "status": "processed"}

# Simulate request handling
request_id.set("req-12345")
process_order(100)
# [req-12345] Entering process_order
# [req-12345] process_order completed successfully

Production Pitfalls ⚠️

1. Quên @wraps → Debug Nightmare

python
# ❌ BAD: Không có @wraps
def bad_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@bad_decorator
def my_function():
    """Important docstring."""
    pass

print(my_function.__name__)  # 'wrapper' - Không biết hàm nào!
print(my_function.__doc__)   # None - Mất documentation!

2. Decorator với Methods - Quên self

python
# ❌ BAD: Decorator không handle self
def log_call(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

class Calculator:
    @log_call
    def add(self, a, b):
        return a + b

calc = Calculator()
calc.add(1, 2)  # ✅ Hoạt động vì *args bắt self

# Nhưng nếu cần access self trong decorator:
def log_with_instance(func):
    @wraps(func)
    def wrapper(self, *args, **kwargs):  # ✅ Explicit self
        print(f"Instance: {self}, calling {func.__name__}")
        return func(self, *args, **kwargs)
    return wrapper

3. Mutable Default Arguments trong Decorator

python
# ❌ BAD: Mutable default shared across all decorated functions
def cache_bad(func, cache={}):  # cache shared!
    @wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

# ✅ GOOD: Cache per function
def cache_good(func):
    cache = {}  # Mỗi decorated function có cache riêng
    @wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

4. Async Functions cần Async Wrapper

python
import asyncio
from functools import wraps

# ❌ BAD: Sync wrapper cho async function
def timer_sync(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        import time
        start = time.time()
        result = func(*args, **kwargs)  # Returns coroutine, không await!
        print(f"Time: {time.time() - start}")
        return result
    return wrapper

# ✅ GOOD: Async-aware decorator
def timer_async(func):
    @wraps(func)
    async def wrapper(*args, **kwargs):
        import time
        start = time.time()
        result = await func(*args, **kwargs)
        print(f"Time: {time.time() - start}")
        return result
    return wrapper

# ✅ BEST: Universal decorator
import inspect

def timer_universal(func):
    if inspect.iscoroutinefunction(func):
        @wraps(func)
        async def async_wrapper(*args, **kwargs):
            import time
            start = time.time()
            result = await func(*args, **kwargs)
            print(f"Time: {time.time() - start}")
            return result
        return async_wrapper
    else:
        @wraps(func)
        def sync_wrapper(*args, **kwargs):
            import time
            start = time.time()
            result = func(*args, **kwargs)
            print(f"Time: {time.time() - start}")
            return result
        return sync_wrapper

Bảng Tóm tắt

python
# === DECORATOR CƠ BẢN ===
from functools import wraps

def decorator(func):
    @wraps(func)  # LUÔN LUÔN dùng!
    def wrapper(*args, **kwargs):
        # Logic trước
        result = func(*args, **kwargs)
        # Logic sau
        return result
    return wrapper

# === DECORATOR FACTORY (với arguments) ===
def decorator_with_args(arg1, arg2="default"):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # Dùng arg1, arg2 ở đây
            return func(*args, **kwargs)
        return wrapper
    return decorator

# === CLASS DECORATOR (stateful) ===
from functools import update_wrapper

class StatefulDecorator:
    def __init__(self, func):
        self.func = func
        self.state = {}
        update_wrapper(self, func)
    
    def __call__(self, *args, **kwargs):
        # Access self.state
        return self.func(*args, **kwargs)

# === CLASS DECORATOR FACTORY ===
class ConfigurableDecorator:
    def __init__(self, config_value):
        self.config = config_value
        self.func = None
    
    def __call__(self, func):
        self.func = func
        update_wrapper(self, func)
        return self
    
    def __get__(self, obj, objtype=None):
        # Support methods
        return lambda *a, **kw: self._call(obj, *a, **kw)

# === DECORATING CLASSES ===
def class_decorator(cls):
    # Modify cls
    cls.new_attribute = "value"
    return cls

# === STACKING ORDER ===
@outer      # Thực thi đầu tiên
@middle     # Thực thi thứ hai
@inner      # Áp dụng đầu tiên, thực thi cuối
def func():
    pass