Hiểu về Python Decorators với Mô hình Ba Lớp

07 tháng 4, 2026·5 phút đọc

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

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 ArchitectureDecorator 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:

  1. Gọi outer(param) → trả về decorator
  2. Gọi decorator(fn) với fn là hàm gốc → trả về wrapper

Hoặc viết rõ ràng:

decorator = outer(param)
greet = decorator(greet)

3-layer Decorator Architecture3-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ọi fn times lầ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 được tổng hợp và biên soạn bằng AI từ các nguồn tin tức công nghệ. Nội dung mang tính tham khảo. Xem bài gốc ↗