Decorator trong Python: là gì, hoạt động thế nào và cách tự viết
Decorator trong Python là hàm nhận một hàm khác và trả về hàm mới, thường là một wrapper bọc ngoài để thêm hành vi trước hoặc sau lần gọi: ghi log, đo thời gian, cache, kiểm tra quyền. Cú pháp @decorator đặt trên def là cách viết ngắn của func = decorator(func); để wrapper giữ được tên và docstring của hàm gốc, người ta dùng functools.wraps.
Decorator trong Python là một hàm nhận vào một hàm khác và trả về một hàm mới, thường là một wrapper bọc quanh hàm gốc. Nhờ vậy có thể thêm hành vi cho hàm — ghi log, đo thời gian, cache, thử lại khi lỗi — mà không sửa code của nó. Decorator được viết bằng cách đặt @tên phía trên khai báo hàm, và xuất hiện khắp nơi: trong Flask và FastAPI, trong pytest, trong thư viện chuẩn.
Decorator là gì, nói đơn giản
Hãy hình dung một món quà và lớp giấy gói. Món quà (hàm) vẫn y nguyên, nhưng bên ngoài có thêm một lớp được kích hoạt đầu tiên. Decorator chính là lớp giấy gói đó: khi hàm được gọi, code của wrapper chạy trước, nó gọi hàm gốc và có thể xử lý kết quả trả về.
Cơ chế này hoạt động vì trong Python, hàm là các đối tượng thông thường. Có thể truyền hàm vào hàm khác, trả về hàm từ một hàm, và khai báo hàm bên trong hàm. Decorator được xây dựng dựa trên ba tính chất đó.
Decorator hoạt động thế nào: @ chỉ là cú pháp rút gọn
Hãy xem một decorator đơn giản in thông báo trước và sau khi hàm được gọi:
def log_calls(func):
def wrapper():
print(f"Đang gọi {func.__name__}")
result = func()
print(f"{func.__name__} đã hoàn thành")
return result
return wrapper
@log_calls
def hello():
print("Xin chào!")
hello()
Đang gọi hello
Xin chào!
hello đã hoàn thành
Dòng @log_calls phía trên def hoàn toàn tương đương với câu lệnh viết sau khai báo:
def hello():
print("Xin chào!")
hello = log_calls(hello)
Lúc này tên hello trỏ tới wrapper, còn hàm gốc nằm bên trong closure — wrapper ghi nhớ biến func của hàm bao ngoài. Điều quan trọng: decorator được áp dụng một lần, tại thời điểm chạy def, tức là khi module được import; còn wrapper chạy mỗi khi hàm được gọi.
Closure và decorator
Closure là hàm lồng bên trong, nhớ các biến của hàm bao ngoài kể cả sau khi hàm bao ngoài đã kết thúc. Không có closure thì không có decorator: chính nhờ nó mà wrapper truy cập được func. Trong closure cũng có thể lưu trạng thái, ví dụ bộ đếm số lần gọi:
def count_calls(func):
calls = 0
def wrapper(*args, **kwargs):
nonlocal calls
calls += 1
print(f"{func.__name__}: lần gọi thứ {calls}")
return func(*args, **kwargs)
return wrapper
Cách viết decorator trong Python: khung chuẩn
Ví dụ đầu tiên có hai nhược điểm: wrapper không nhận tham số, và nó «nuốt» tên của hàm gốc. Khung làm việc đúng, nên bắt đầu từ đó khi viết bất kỳ decorator nào, như sau:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# code trước khi gọi
result = func(*args, **kwargs)
# code sau khi gọi
return result
return wrapper
Khung này có ba quy tắc:
*args, **kwargs— wrapper nhận mọi đối số và chuyển tiếp nguyên vẹn.return result— đừng quên trả về kết quả, nếu không hàm được decorator sẽ luôn trả vềNone.@functools.wraps(func)— chuyển metadata của hàm gốc sang wrapper.
Ví dụ decorator đo thời gian chạy:
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} mất {elapsed:.3f} s")
return result
return wrapper
@timer
def slow_sum(n):
return sum(range(n))
slow_sum(10_000_000) # slow_sum mất 0.142 s
Vì sao cần functools.wraps
Nếu không có wraps, hàm được decorator sẽ bị đổi tên, docstring và các thuộc tính khác — bên ngoài chỉ thấy wrapper:
def plain(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@plain
def add(a, b):
"""Cộng hai số."""
return a + b
print(add.__name__, add.__doc__) # wrapper None
Với @functools.wraps(func), wrapper nhận các thuộc tính __name__, __doc__, __module__, __qualname__, __annotations__ của hàm gốc, và thuộc tính __wrapped__ giữ tham chiếu tới bản gốc. Điều này quan trọng khi gỡ lỗi và đọc traceback, khi dùng help(), với các framework đăng ký hàm theo tên, và với inspect.signature để lấy đúng chữ ký hàm.
Decorator có tham số
Đôi khi decorator cần cấu hình: @retry(times=3), @app.route("/"). Khi đó người ta thêm một tầng lồng nhau. Hàm bên ngoài nhận tham số và trả về decorator thực sự — cấu trúc này gọi là factory của decorator.
import functools
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def ping():
print("ping")
ping() # ping, ping, ping — ba dòng
Thứ tự thực thi: repeat(times=3) trả về decorator, decorator này được áp dụng cho ping và trả về wrapper. Nói cách khác, @repeat(times=3) tương đương với ping = repeat(times=3)(ping).
Ví dụ thực tế hơn — thử lại khi gặp lỗi:
import functools
import time
def retry(times=3, delay=1.0, exceptions=(Exception,)):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except exceptions:
if attempt == times:
raise
time.sleep(delay)
return wrapper
return decorator
@retry(times=5, delay=0.5, exceptions=(ConnectionError,))
def fetch_rate():
...
Nhiều decorator trên cùng một hàm
Có thể đặt nhiều decorator chồng lên nhau. Chúng được áp dụng từ dưới lên: decorator gần def nhất được áp dụng đầu tiên.
@timer
@log_calls
def report():
...
# tương đương:
report = timer(log_calls(report))
Khi gọi, lớp ngoài cùng (timer) chạy đầu tiên, nên nó sẽ đo cả thời gian chạy của log_calls. Thứ tự rất quan trọng, ví dụ khi một decorator kiểm tra quyền truy cập và một decorator khác cache kết quả.
Các decorator có sẵn và tiêu chuẩn của Python
Nhiều decorator hữu ích đã có sẵn trong ngôn ngữ và thư viện chuẩn:
| Decorator | Tác dụng |
|---|---|
@property | biến phương thức thành thuộc tính chỉ đọc; kết hợp @x.setter thì thành thuộc tính có kiểm soát gán giá trị |
@staticmethod | phương thức của class không có self, chỉ là hàm thông thường nằm trong không gian tên của class |
@classmethod | phương thức nhận class cls thay vì instance — thường dùng để viết constructor thay thế |
@functools.cache, @functools.lru_cache | cache kết quả theo đối số |
@functools.wraps | sao chép metadata của hàm sang wrapper |
@dataclasses.dataclass | sinh __init__, __repr__, __eq__ cho class |
@contextlib.contextmanager | biến một generator thành context manager dùng với with |
Cache: functools.cache và lru_cache
Decorator cache ghi nhớ kết quả ứng với mỗi tập đối số, và khi gọi lại với cùng đối số thì trả về kết quả đã lưu mà không tính lại. Ví dụ kinh điển là dãy Fibonacci đệ quy, nếu không có cache thì thời gian tăng theo cấp số nhân:
import functools
@functools.cache
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(100)) # 354224848179261915075
functools.cache (có từ Python 3.9) lưu mọi kết quả không giới hạn. @functools.lru_cache(maxsize=128) chỉ giữ một số bản ghi gần nhất và loại bỏ các bản cũ — an toàn hơn cho dịch vụ chạy lâu dài. Xem thống kê bằng fib.cache_info(), xóa cache bằng fib.cache_clear(). Đối số của các hàm này phải băm được: truyền list sẽ gây TypeError: unhashable type: 'list'. Và chỉ nên cache các hàm thuần túy — hàm luôn trả cùng kết quả với cùng đối số và không có tác dụng phụ.
Decorator property làm gì
@property cho phép truy cập một phương thức như một thuộc tính, không cần dấu ngoặc, và khi cần thì kiểm soát được việc gán giá trị:
class Account:
def __init__(self, balance):
self._balance = balance
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, value):
if value < 0:
raise ValueError("Số dư không thể âm")
self._balance = value
acc = Account(100)
print(acc.balance) # 100
acc.balance = -5 # ValueError
staticmethod và classmethod
class Money:
def __init__(self, kopecks):
self.kopecks = kopecks
@classmethod
def from_rubles(cls, rubles):
return cls(round(rubles * 100))
@staticmethod
def is_valid(value):
return value >= 0
m = Money.from_rubles(12.5)
print(m.kopecks, Money.is_valid(-1)) # 1250 False
Chính @classmethod cho phép phương thức làm việc với bản thân class thay vì các instance của nó: from_rubles sẽ tạo đúng object của lớp con nếu được gọi từ lớp con.
Decorator cho class và class làm decorator
Decorator có thể áp dụng cho class: nó nhận class và trả về class (cùng class hoặc class đã chỉnh sửa). Cơ chế của @dataclass là như vậy. Ví dụ đơn giản — một registry các class:
registry = {}
def register(cls):
registry[cls.__name__] = cls
return cls
@register
class CsvExporter:
pass
print(registry) # {'CsvExporter': <class '__main__.CsvExporter'>}
Ngược lại, một class cũng có thể đóng vai decorator. Khi đó chỉ cần một object có phương thức __call__; trạng thái được lưu trong thuộc tính của instance:
import functools
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.calls = 0
def __call__(self, *args, **kwargs):
self.calls += 1
return self.func(*args, **kwargs)
@CountCalls
def tick():
pass
tick(); tick()
print(tick.calls) # 2
Decorator bất đồng bộ
Với hàm async def, wrapper cũng phải là coroutine và phải chờ kết quả bằng await; nếu không, nó sẽ trả về một coroutine chưa được thực thi:
import functools
def log_async(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
print(f"Bắt đầu {func.__name__}")
result = await func(*args, **kwargs)
print(f"Xong {func.__name__}")
return result
return wrapper
Vì sao cần decorator trong Python
- Logic dùng chung — ghi log, đo số liệu, đo thời gian mà không phải chép code vào từng hàm.
- Cache —
@functools.cachecho các phép tính đắt đỏ nhưng thuần túy. - Kiểm tra — quyền truy cập, kiểm tra tham số, xác thực trong các handler web.
- Đăng ký — route trong Flask và FastAPI (
@app.get("/")), fixture trong pytest. - Độ tin cậy — thử lại, timeout, xử lý lỗi.
Lợi ích lớn nhất là tách biệt trách nhiệm: hàm chỉ làm đúng việc của nó, còn hành vi phụ được gắn vào bằng một dòng và cũng dễ dàng gỡ bỏ.
Lỗi thường gặp
- Wrapper không trả về kết quả — hàm sau khi decorate trả về
None. - Thiếu
*args, **kwargs—TypeErrorkhi gọi có đối số. - Quên
wraps— sai__name__và docstring, lộn xộn trong log. - Thiếu dấu ngoặc ở factory — viết
@repeatthay vì@repeat(3): hàm sẽ bị truyền vào tham sốtimes. - Code nặng trong thân decorator — nó chạy khi import module, không phải khi gọi hàm.
Tổng kết
Decorator trong Python là hàm bậc cao: nhận một hàm, trả về wrapper, còn @ chỉ là cách viết ngắn gọn của phép gán. Hãy bắt đầu với khung có *args, **kwargs, return result và functools.wraps, và thêm một tầng factory khi cần tham số. Hàm, closure và chú thích — những thứ làm nên decorator — được thực hành trong khóa học Python.
Câu hỏi thường gặp
Có thể gỡ decorator khỏi hàm không?
Nếu decorator dùng functools.wraps, bản gốc có sẵn qua thuộc tính __wrapped__: func.__wrapped__(...) gọi hàm mà không qua wrapper. Nếu không, chỉ còn cách bỏ dòng có @ trong code.
Code của decorator chạy lúc nào?
Thân của chính decorator chạy một lần, khi khai báo hàm, thường trong lúc import module. Code bên trong wrapper chạy mỗi khi hàm được decorate được gọi.
Có thể decorate phương thức của class không?
Được, decorator thông thường với *args, **kwargs cũng hoạt động với phương thức: self chỉ đơn giản là phần tử đầu tiên trong args.
Decorator khác kế thừa ở điểm nào?
Kế thừa thay đổi hành vi của class qua cấu trúc phân cấp, còn decorator bọc bên ngoài một hàm hoặc class cụ thể và có thể kết hợp với các decorator khác mà không cần sửa code.
Chủ đề này được giảng chi tiết kèm thực hành trong khóa học “Python 3.15: от новичка до профи”.
Đến khóa học