🌱 [Python] 15 - Decorators — Cách hoạt động & ví dụ thực tế

🌱 [Python] 15 - Decorators — Cách hoạt động & ví dụ thực tế

    Decorators trong Python là một trong những công cụ mạnh mẽ nhất của ngôn ngữ này. Chúng cho phép bạn mở rộng hoặc thay đổi hành vi của function/class mà không cần chỉnh sửa trực tiếp code gốc. Hiểu rõ về decorators sẽ là tiền đề giúp bạn nắm vững các framework như Flask, Django, hay pytest.

1 - Decorator, cú pháp và cách hoạt động?

    Decorator là một hàm nhận vào một hàm khác như đối số và trả về một hàm mới với hành vi được mở rộng. Nó thường được sử dụng để thêm logic logging, kiểm tra quyền, đo thời gian thực thi, caching, v.v...

  1. def decorator_func(func):
  2. def wrapper():
  3. print("Before function call")
  4. func()
  5. print("After function call")
  6. return wrapper

  7. @decorator_func
  8. def say_hello():
  9. print("Hello!")

  10. say_hello()
  11. # Output:
  12. # Before function call
  13. # Hello!
  14. # After function call

Cú pháp và cách hoạt động

    Khi bạn viết @decorator_func trên một hàm, Python sẽ tự động thực hiện: say_hello = decorator_func(say_hello). Vì vậy decorator thực chất chỉ là cách viết ngắn gọn, giúp code dễ đọc hơn.

2 - Stacking & Multiple Decorators

    Bạn có thể chồng nhiều decorator lên cùng một hàm. Chúng được áp dụng (được "gắn") từ trên xuống dưới tại thời điểm định nghĩa hàm, nhưng khi gọi, các wrapper chạy theo thứ tự ngược lại (tức là từ dưới lên).

  1. def deco1(f):
  2. def wrap():
  3. print("deco1")
  4. f()
  5. return wrap

  6. def deco2(f):
  7. def wrap():
  8. print("deco2")
  9. f()
  10. return wrap

  11. @deco1
  12. @deco2
  13. def hello():
  14. print("hello")

  15. hello()
  16. # Output:
  17. # deco1
  18. # deco2
  19. # hello

    ↪ Giải thích chi tiết:
  • Thời điểm "decoration" (khi Python đọc định nghĩa hàm):
    • Khi Python gặp @deco2 phía trên def hello, nó thực hiện hello = deco2(hello). Kết quả là hello trở thành hàm wrap trả về từ deco2.
    • Sau đó gặp @deco1, Python thực hiện hello = deco1(hello), tức hello giờ là hàm wrap trả về từ deco1, và bên trong nó giữ một tham chiếu tới hàm được trả về bởi deco2.
    • Nên thứ tự gắn là: hello = deco1(deco2(hello_original)).
  • Thời điểm gọi (call time):
    • Khi bạn gọi hello(), bạn đang gọi wrapper bên ngoài (do deco1 tạo). Bên ngoài in deco1, sau đó gọi hàm nội tại (chính là wrapper được tạo bởi deco2).
    • Wrapper của deco2 in deco2, rồi gọi hàm gốc hello (in hello).
    • Kết quả thực thi theo chuỗi: deco1 → deco2 → hello.
  • Vấn đề thường gặp:
    • Nếu wrapper không chấp nhận *args, **kwargs nhưng hàm gốc có tham số, sẽ xảy ra TypeError. Luôn viết wrapper dạng tổng quát nếu muốn áp dụng cho nhiều hàm khác nhau.
    • Nếu không dùng functools.wraps, metadata của hàm (như __name__, __doc__, signature) sẽ mất — làm khó debug và introspection.

Phiên bản cải tiến (an toàn hơn, general-purpose):

  1. from functools import wraps

  2. def deco1(f):
  3. @wraps(f)
  4. def wrap(*args, **kwargs):
  5. print("deco1")
  6. return f(*args, **kwargs)
  7. return wrap

  8. def deco2(f):
  9. @wraps(f)
  10. def wrap(*args, **kwargs):
  11. print("deco2")
  12. return f(*args, **kwargs)
  13. return wrap

  14. @deco1
  15. @deco2
  16. def hello():
  17. print("hello")

  18. hello()
  19. # Output:
  20. # deco1
  21. # deco2
  22. # hello

    Với *args, **kwargs@wraps, decorator an toàn hơn và giữ metadata.


3 - Decorator có tham số

    Khi bạn muốn truyền tham số cho decorator (ví dụ @repeat(3)), cần một "decorator factory": hàm ngoài nhận tham số và trả về một decorator thực sự.

  1. def repeat(n):
  2. def decorator(func):
  3. def wrapper(*args, **kwargs):
  4. for _ in range(n):
  5. func(*args, **kwargs)
  6. return wrapper
  7. return decorator

  8. @repeat(3)
  9. def greet():
  10. print("Hi!")

  11. greet()
  12. # Output: Hi! (in ra 3 lần)

Giải thích chi tiết:
  • Cấu trúc ba lớp:
    1. repeat(n) là hàm factory ➜ lúc này gọi repeat(3) và nhận về một decorator.
    2. decorator(func) là decorator thực sự ➜ nhận hàm cần bọc và trả về wrapper.
    3. wrapper(*args, **kwargs) là hàm wrapper khi gọi ➜ thực hiện logic lặp n lần rồi gọi hàm gốc.
  • Closure: giá trị n được "bắt" (captured) trong closure của wrapper, nên wrapper nhớ số lần lặp mà bạn cấu hình tại thời điểm sử dụng decorator.
  • Trả về giá trị:
    • Trong ví dụ trên, wrapper không trả về kết quả của hàm gốc (mặc định trả None), vì ta gọi hàm gốc nhiều lần và bỏ qua kết quả. Nếu hàm gốc có giá trị trả về quan trọng, bạn cần xác định hành vi mong muốn (ví dụ: trả về kết quả lần cuối, trả list các kết quả, hoặc gộp kết quả).
  • Phiên bản cải tiến (trả về kết quả lần cuối và bảo toàn metadata):
    1. from functools import wraps

    2. def repeat(n):
    3. def decorator(func):
    4. @wraps(func)
    5. def wrapper(*args, **kwargs):
    6. result = None
    7. for _ in range(n):
    8. result = func(*args, **kwargs)
    9. return result
    10. return wrapper
    11. return decorator

    12. @repeat(3)
    13. def greet():
    14. print("Hi!")

    15. greet()
    16. # Output: Hi! (in ra 3 lần)

    Ở đây wrapper trả result từ lần gọi cuối cùng, phù hợp khi hàm có side-effect và bạn muốn kết quả cuối cùng.

  • Lưu ý:
    • Nếu hàm gốc có side-effect (ví dụ gửi request), lặp nhiều lần có thể không mong muốn → cần cân nhắc (exponential backoff, stop-on-success...).
    • Đối với các hàm IO/Network, thường kết hợp retry với delay/backoff và điều kiện dừng (stop-on-success hoặc chỉ retry với các exception cụ thể).

4 - Class Decorators

    Decorator cũng có thể được viết bằng class, rất hữu ích khi bạn muốn lưu trạng thái giữa các lần gọi (ví dụ đếm số lần gọi). Class decorator hoạt động vì class là callable (khi cài __call__).

  1. class Counter:
  2. def __init__(self, func):
  3. self.func = func
  4. self.count = 0

  5. def __call__(self, *args, **kwargs):
  6. self.count += 1
  7. print(f"Call {self.count}")
  8. return self.func(*args, **kwargs)

  9. @Counter
  10. def hello():
  11. print("Hi")

  12. hello()
  13. hello()
  14. # Output:
  15. # Call 1
  16. # Hi
  17. # Call 2
  18. # Hi

Giải thích chi tiết:
  • Khi áp dụng @Counter:
    • Python gọi Counter(hello) → tức là tạo một instance của class Counter và truyền hàm gốc vào __init__.
    • Instance trả về được gán thay cho tên hàm hello. Vì instance có phương thức __call__, nó trở thành một callable thay thế cho hàm ban đầu.
  • Trạng thái được lưu: Giá trị self.count nằm trên instance decorator, vì vậy nó tồn tại và được tăng dần qua nhiều lần gọi, đây là lợi ích chính của decorator dạng class so với hàm: dễ lưu state.
  • Gọi hàm: Khi bạn gọi hello(), thực tế Python gọi Counter_instance.__call__(). Trong __call__ ta tăng bộ đếm rồi gọi self.func(*args, **kwargs) để thực thi hàm gốc.
  • Preserve metadata / best practice:
    • Để giữ metadata (tên hàm, docstring), trong __init__ bạn nên gọi functools.update_wrapper(self, func) hoặc dùng wraps(func)(self). Nếu không, introspection (ví dụ help() hoặc inspect.signature()) sẽ thấy instance thay vì hàm gốc.
    from functools import update_wrapper

    class Counter:
    def __init__(self, func):
    self.func = func
    self.count = 0
    update_wrapper(self, func) # copy __name__, __doc__...
    def __call__(self, *args, **kwargs):
    self.count += 1
    print(f"Call {self.count}")
    return self.func(*args, **kwargs)
  • Concurrency / Thread-safety:
    • Nếu ứng dụng đa luồng, self.count += 1 có thể race condition. Nếu cần chính xác, thêm khóa (lock) để bảo vệ state.
  • Ưu điểm của cách làm này là dễ lưu lại trạng thái, có thể chứa nhiều logic phức tạp, cấu trúc rõ ràng. Tuy nhiên, instance thay thế cho hàm nên đôi khi làm khó debug nếu không dùng update_wrapper; chi phí nhỏ hơn so với function decorator về readability trong một số trường hợp.
Tổng kết nhanh / checklist khi viết decorator:
  • Dùng *args, **kwargs trong wrapper để tương thích với mọi signature.
  • Dùng functools.wraps (hoặc functools.update_wrapper) để bảo toàn metadata.
  • Quan tâm đến thứ tự khi chồng decorator → thứ tự gắn ≠ thứ tự chạy.
  • Với decorator có tham số, nhớ pattern factory → decorator → wrapper.
  • Nếu cần lưu state, cân nhắc dùng class-based decorator và xử lý thread-safety nếu cần.
  • Luôn cân nhắc return value khi wrapper gọi hàm nhiều lần (ví dụ repeat/retry): phải rõ ràng trả cái gì.

5 - Xây dựng decorator retry hoàn chỉnh (với backoff & logging)

    Một use-case thực tế của decorator là xử lý retry cho các thao tác có thể thất bại tạm thời, như request HTTP hoặc truy cập database. Dưới đây là một ví dụ hoàn chỉnh có hỗ trợ retry count, delay tăng dần (exponential backoff), và logging.

  1. import time
  2. import logging

  3. logging.basicConfig(level=logging.INFO)

  4. def retry(max_attempts=3, backoff=1.0):
  5. def decorator(func):
  6. def wrapper(*args, **kwargs):
  7. attempt = 1
  8. delay = backoff
  9. while attempt <= max_attempts:
  10. try:
  11. return func(*args, **kwargs)
  12. except Exception as e:
  13. logging.warning(f"Attempt {attempt} failed: {e}")
  14. if attempt == max_attempts:
  15. logging.error("Max retries exceeded")
  16. raise
  17. time.sleep(delay)
  18. delay *= 2 # exponential backoff
  19. attempt += 1
  20. return wrapper
  21. return decorator

  22. @retry(max_attempts=4, backoff=0.5)
  23. def unstable_func():
  24. import random
  25. if random.random() < 0.7:
  26. raise ValueError("Random failure")
  27. print("Success!")

  28. unstable_func()

    Trong ví dụ trên: – Hàm sẽ retry tối đa 4 lần, mỗi lần lỗi sẽ chờ lâu hơn gấp đôi trước khi thử lại. Tất cả kết quả được log ra console. Đây là một pattern rất phổ biến khi xử lý API, network hoặc sensor data.

Unit test mẫu:

  1. import unittest

  2. class TestRetryDecorator(unittest.TestCase):
  3. def test_success(self):
  4. calls = {"count": 0}
  5. @retry(max_attempts=3)
  6. def func():
  7. calls["count"] += 1
  8. if calls["count"] < 2:
  9. raise ValueError("fail")
  10. return "ok"

  11. result = func()
  12. self.assertEqual(result, "ok")
  13. self.assertEqual(calls["count"], 2)

  14. if __name__ == "__main__":
  15. unittest.main()

6 - Kết luận

    Decorators là một phần quan trọng trong Python giúp code sạch, dễ mở rộng và có khả năng tái sử dụng cao. Khi hiểu cách chúng hoạt động, bạn có thể áp dụng để viết logging, retry, validation, hoặc các pattern phức tạp khác một cách hiệu quả.

>>>>>> Follow ngay <<<<<<<

Để nhận được những bài học miễn phí mới nhất nhé 😊
Chúc các bạn học tập tốt 😊

Nguyễn Văn Nghĩa

Mình là một người thích học hỏi và chia sẻ các kiến thức về Nhúng IOT.

Đăng nhận xét

Mới hơn Cũ hơn
//