Hiểu về Python Decorators với Mô hình Ba Lớp
Bài viết hướng dẫn chi tiết về decorators trong Python, từ cách hoạt động cơ bản đến cách viết decorators có tham số theo mô hình ba lớp. Qua đó, bạn có thể áp dụng các decorator hữu ích như @retry để xử lý lỗi mạng, @timer đo thời gian thực thi và @require_role để bảo vệ API.

Hiểu về Python Decorators với Mô hình Ba Lớp
Decorators trong Python là một tính năng mạnh mẽ và thường xuyên được sử dụng giúp thay đổi hành vi của hàm chỉ qua một dòng mã đặt phía trên hàm đó, ví dụ như @login_required, @cache, hoặc @app.route("/"). Tuy nhiên, khi cần thêm tham số cho decorator như @retry(times=3) hay @require_role("admin"), nhiều lập trình viên thường gặp khó khăn.
Bài viết này sẽ giúp bạn hiểu sâu hơn về cách hoạt động của decorators, đặc biệt là mô hình ba lớp cho decorators có tham số, để bạn có thể tự tin áp dụng và viết ra các decorators hữu ích cho dự án thực tế.
Decorators hoạt động thế nào dưới 'nắp máy'?
Về mặt kỹ thuật, decorator là một hàm nhận vào một hàm khác và trả về một hàm mới. Ví dụ hàm decorator đơn giản:
def shout(fn):
def wrapper(*args, **kwargs):
result = fn(*args, **kwargs)
return str(result).upper()
return wrapper
@shout
def greet(name):
"""Chào người dùng"""
return f"hello, {name}"
print(greet("alice")) # In ra HELLO, ALICE
Cú pháp @shout thực chất là cách viết tắt của:
greet = shout(greet)
Điều này có nghĩa mỗi lần gọi greet(...) là gọi hàm wrapper(...) được shout tạo ra, hàm này gọi hàm gốc bên trong.
Tuy nhiên, sau khi decorator thay thế hàm gốc, các thuộc tính như tên hàm __name__ và tài liệu __doc__ bị mất hoặc bị đổi thành của hàm wrapper. Để khắc phục, Python cung cấp functools.wraps:
import functools
def shout(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
result = fn(*args, **kwargs)
return str(result).upper()
return wrapper
Bằng cách này, danh tính hàm gốc được giữ lại, giúp debug, logging, và test hiệu quả hơn.
Decorator Architecture
Mô hình ba lớp cho decorator có tham số
Decorator đơn giản chỉ có một lớp (layer): nhận hàm và trả về hàm.
Khi bạn muốn thêm tham số cho decorator, bạn cần thêm một lớp bên ngoài nữa để nhận tham số đó, rồi trả về một decorator bình thường. Như vậy, tổng cộng sẽ có ba lớp chức năng:
def outer(param): # Lớp 1: nhận tham số của decorator
def decorator(fn): # Lớp 2: nhận hàm cần trang trí
def wrapper(*args, **kwargs): # Lớp 3: chạy mỗi lần hàm được gọi
# param vẫn có thể sử dụng ở đây do closure
return fn(*args, **kwargs)
return wrapper
return decorator
Khi có dòng @outer(param), Python thực hiện 2 bước:
- Gọi
outer(param)→ trả vềdecorator - Gọi
decorator(fn)vớifnlà hàm gốc → trả vềwrapper
Hoặc viết rõ ràng:
decorator = outer(param)
greet = decorator(greet)
3-layer Decorator Architecture
Giải thích:
- Lớp ngoài cùng (factory) chạy một lần lúc decorator được áp dụng — nhận tham số và trả về decorator.
- Lớp giữa nhận hàm gốc, cũng chạy một lần.
- Lớp trong cùng là hàm wrapper gọi hàm gốc thực sự, chạy mỗi lần hàm được gọi.
Ví dụ: viết decorator @repeat(times=3)
Hãy viết decorator lặp lại lời gọi hàm nhiều lần:
import functools
def repeat(times):
def decorator(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
for _ in range(times):
result = fn(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
# In ra:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!
Ở đây:
repeat(times)là factory nhận tham sốtimes.decorator(fn)nhận hàm gốc.wrapper(...)gọifntimeslần.
Các decorator tham số hữu ích trong thực tế
@retry – tự động thử lại khi lỗi
import time
import functools
def retry(times=3, delay=1.0, exceptions=(Exception,)):
def decorator(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
last_exc = None
for attempt in range(1, times + 1):
print(f"Attempt {attempt} of {times}")
try:
return fn(*args, **kwargs)
except exceptions as e:
last_exc = e
if attempt < times:
time.sleep(delay)
raise last_exc
return wrapper
return decorator
Decorators này giúp xử lý các thao tác dễ lỗi như gọi API, kết nối mạng bị gián đoạn… bằng cách tự retry nhiều lần trước khi thôi.
@timer – đo thời gian thực thi hàm
import time
import functools
def timer(unit="ms"):
units = {"ms": 1000, "s": 1, "us": 1000000}
def decorator(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = fn(*args, **kwargs)
elapsed = (time.perf_counter() - start) * units[unit]
print(f"{fn.__name__} took {elapsed:.2f}{unit}")
return result
return wrapper
return decorator
Đo thời gian giúp phát hiện bottleneck khi phát triển hoặc giám sát sản phẩm.
@require_role – kiểm tra quyền người dùng
import functools
class User:
def __init__(self, name, role):
self.name = name
self.role = role
def require_role(role):
def decorator(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
user = kwargs.get("user") or (args[0] if args else None)
if user is None or user.role != role:
raise PermissionError(f"Role '{role}' required.")
return fn(*args, **kwargs)
return wrapper
return decorator
Dùng nhiều trong các framework web để bảo vệ route hoặc API dựa vào vai trò.
Có thể chồng các decorator với nhau?
Hoàn toàn được. Ví dụ:
@retry(times=3, delay=0.5)
@timer(unit="ms")
def fetch_data(url):
...
Nhưng bạn cần lưu ý rằng decorator được áp dụng từ dưới lên theo thứ tự viết, và thứ tự này ảnh hưởng đến thứ tự gọi các wrapper tại runtime.
Kết luận
- Decorators có tham số thực chất được tạo ra từ một factory bên ngoài, trả về decorator nhận hàm.
- Mô hình ba lớp (factory, decorator, wrapper) là thiết kế chuẩn cho decorator có tham số.
- Luôn sử dụng
@functools.wrapsđể duy trì danh tính hàm gốc, giúp debug, test dễ dàng. - Sắp xếp thứ tự decorator hợp lý khi chồng nhiều decorator tránh hiểu nhầm khi thực thi.
- Hiểu rõ sự khác biệt giữa thời điểm chạy lúc trang trí (decoration time) và thực thi hàm (call time) để tránh lỗi khó phát hiện.
Một khi nắm được mẫu này, bạn có thể dễ dàng viết các decorators mạnh mẽ, sạch sẽ và tái sử dụng trong nhiều dự án.
Nếu bạn quan tâm sâu hơn, mã nguồn minh họa đầy đủ của bài viết có trên GitHub: https://github.com/nunombispo/python-decorators-article
Bài viết liên quan

Phần mềm
Anthropic ra mắt Claude Opus 4.7: Nâng cấp mạnh mẽ cho lập trình nhưng vẫn thua Mythos Preview
16 tháng 4, 2026

Công nghệ
Qwen3.6-35B-A3B: Quyền năng Lập trình Agentic, Nay Đã Mở Cửa Cho Tất Cả
16 tháng 4, 2026

Công nghệ
Spotify thắng kiện 322 triệu USD từ nhóm pirate Anna's Archive nhưng đối mặt với bài toán thu hồi
16 tháng 4, 2026
