Giao diện
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àmCú 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ì?
| Attribute | Mô 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 * bRetry 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:05Singleton 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) # TrueRegistry 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: afterVisualizing 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") # ❌ TypeError2. 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 successfullyProduction 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 wrapper3. 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 wrapper4. 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_wrapperBả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():
passCross-links
- Prerequisites: Functions & Closures
- Related: Descriptors & Properties - Descriptor protocol
- Related: functools Module (Phase 2) -
lru_cache,partial,wraps - Next: Generators & Iterators