Decorator trong Python: Từ Hàm Bậc Cao đến Cú Pháp Đường

Để hiểu sâu sắc về decorators trong Python, chúng ta sẽ khám phá ba khái niệm cốt lõi theo trình tự: Hàm bậc cao (Higher-Order Functions), Currying, và cuối cùng là Decorator.

Hàm Bậc Cao (Higher-Order Functions)

Trong Python, các hàm được coi là 'công dân hạng nhất' (first-class citizens). Điều này có nghĩa là chúng có thể được gán cho biến, truyền làm đối số cho hàm khác, hoặc được trả về bởi một hàm khác, giống như bất kỳ kiểu dữ liệu nào khác. Một hàm bậc cao là một hàm thực hiện ít nhất một trong hai điều sau:
  1. Nhận một hoặc nhiều hàm khác làm đối số.
  2. Trả về một hàm khác làm kết quả.
Ví dụ về một hàm bậc cao là closure. Hãy xem xét ví dụ sau, trong đó tao_bo_dem là một hàm bậc cao trả về hàm tang_len:
def tao_bo_dem(gia_tri_khoi_tao):
    dem = gia_tri_khoi_tao
    def tang_len(buoc=1):
        nonlocal dem # Sử dụng nonlocal để sửa đổi biến 'dem' của hàm bao ngoài
        dem += buoc
        return dem
    return tang_len

# Sử dụng closure
dem_tu_5 = tao_bo_dem(5)
print(dem_tu_5())    # Output: 6
print(dem_tu_5(2))   # Output: 8
Nhiều hàm tích hợp sẵn của Python cũng là hàm bậc cao, ví dụ như sorted(), filter(), map(). Hàm sorted():
items = [("apple", 10), ("banana", 5), ("orange", 8)]
# Sắp xếp danh sách các tuple dựa trên phần tử thứ hai (giá trị số)
sorted_items_by_value = sorted(items, key=lambda item: item[1])
print(sorted_items_by_value) # Output: [('banana', 5), ('orange', 8), ('apple', 10)]
Hàm filter():
numbers = range(1, 11)
# Lọc các số chẵn từ danh sách
even_numbers_generator = filter(lambda num: num % 2 == 0, numbers)
print(list(even_numbers_generator)) # Output: [2, 4, 6, 8, 10]
Hàm map():
values = [1, 2, 3, 4, 5]
# Nhân đôi và cộng 1 cho mỗi giá trị trong danh sách
transformed_values_generator = map(lambda val: val * 2 + 1, values)
print(list(transformed_values_generator)) # Output: [3, 5, 7, 9, 11]

Currying (Phép Currying)

Currying là một kỹ thuật biến đổi một hàm nhận nhiều đối số thành một chuỗi các hàm, mỗi hàm chỉ nhận một đối số. Nói cách khác, một hàm f(x, y) có thể được chuyển đổi thành f(x)(y). Ví dụ, hàm tinh_tong dưới đây ban đầu nhận hai đối số:
# Hàm gốc
def tinh_tong(a, b):
    return a + b

# Hàm được Currying
def tinh_tong_curried(a):
    def _tinh_tong(b):
        return a + b
    return _tinh_tong

# Gọi hàm
print(tinh_tong_curried(7)(3)) # Output: 10

Từ Currying đến Decorator

Hãy tưởng tượng chúng ta có một hàm tinh_toan_ket_qua đơn giản và muốn thêm chức năng ghi log mỗi khi hàm này được gọi mà không làm thay đổi mã nguồn của nó.

Vấn đề ban đầu: Gắn kết cao

Chèn log trực tiếp vào hàm làm tăng sự gắn kết (coupling) giữa logic nghiệp vụ và logic phụ trợ.
def tinh_toan_ket_qua(so_a, so_b):
    print("Đang thực hiện tính toán...") # Logic ghi log xen lẫn logic nghiệp vụ
    return so_a * so_b

Giải pháp 1: Tách rời chức năng (không linh hoạt về đối số)

Chúng ta có thể tạo một hàm riêng để xử lý việc ghi log và truyền tinh_toan_ket_qua vào đó.
def ghi_log_don_gian(ham_goc):
    print("Bắt đầu ghi log.")
    ket_qua = ham_goc(10, 5) # Đối số cố định, không linh hoạt
    print("Kết thúc ghi log.")
    return ket_qua

def tinh_toan_ket_qua(so_a, so_b):
    return so_a * so_b

print(ghi_log_don_gian(tinh_toan_ket_qua))
# Output:
# Bắt đầu ghi log.
# Kết thúc ghi log.
# 50
Vấn đề là hàm ghi_log_don_gian gọi ham_goc với các đối số cố định. Để linh hoạt hơn, chúng ta cần truyền các đối số của ham_goc vào ghi_log_don_gian.

Giải pháp 2: Truyền đối số linh hoạt với *args và **kwargs

Sử dụng *args và **kwargs cho phép ghi_log_linh_hoat chuyển tiếp mọi đối số đến ham_goc.
def ghi_log_linh_hoat(ham_goc, *args, **kwargs):
    print("Bắt đầu thực thi hàm.")
    ket_qua = ham_goc(*args, **kwargs)
    print("Kết thúc thực thi hàm.")
    return ket_qua

def tinh_toan_ket_qua(so_a, so_b):
    return so_a * so_b

print(ghi_log_linh_hoat(tinh_toan_ket_qua, 20, so_b=5))
# Output:
# Bắt đầu thực thi hàm.
# Kết thúc thực thi hàm.
# 100
Giải pháp này đã tách biệt logic và linh hoạt về đối số. Tuy nhiên, cách gọi vẫn là ghi_log_linh_hoat(tinh_toan_ket_qua, ...) thay vì gọi trực tiếp tinh_toan_ket_qua(...). Đây là lúc Currying và hàm bậc cao phát huy tác dụng.

Giải pháp 3: Sử dụng Currying và Hàm Bậc Cao (Decorator cơ bản)

Chúng ta muốn ghi_log_linh_hoat nhận hàm gốc và trả về một hàm mới có khả năng nhận đối số và thực thi hàm gốc với log. Điều này biến ghi_log_linh_hoat thành một hàm bậc cao, thực hiện currying.
def ghi_log_decorator(ham_goc):
    def ham_bao(*args, **kwargs):
        print(f"[{ham_goc.__name__}] - Bắt đầu thực thi.")
        ket_qua = ham_goc(*args, **kwargs)
        print(f"[{ham_goc.__name__}] - Kết thúc thực thi.")
        return ket_qua
    return ham_bao

def tinh_toan_ket_qua(so_a, so_b):
    return so_a * so_b

# Áp dụng decorator
tinh_toan_ket_qua = ghi_log_decorator(tinh_toan_ket_qua)

print(tinh_toan_ket_qua(15, so_b=4))
# Output:
# [tinh_toan_ket_qua] - Bắt đầu thực thi.
# [tinh_toan_ket_qua] - Kết thúc thực thi.
# 60
Bây giờ, khi chúng ta gọi tinh_toan_ket_qua(15, so_b=4), thực chất chúng ta đang gọi ham_bao được trả về bởi ghi_log_decorator, và ham_bao này sẽ thực hiện chức năng ghi log trước và sau khi gọi hàm tinh_toan_ket_qua gốc. Đây chính là bản chất của một decorator.

Giải pháp cuối cùng: Cú pháp đường (Syntactic Sugar) @

Python cung cấp một cú pháp tiện lợi để áp dụng decorator bằng cách sử dụng ký hiệu @.
def ghi_log_decorator(ham_goc):
    def ham_bao(*args, **kwargs):
        print(f"[{ham_goc.__name__}] - Bắt đầu thực thi.")
        ket_qua = ham_goc(*args, **kwargs)
        print(f"[{ham_goc.__name__}] - Kết thúc thực thi.")
        return ket_qua
    return ham_bao

@ghi_log_decorator # Tương đương với: tinh_toan_ket_qua = ghi_log_decorator(tinh_toan_ket_qua)
def tinh_toan_ket_qua(so_a, so_b):
    return so_a * so_b

print(tinh_toan_ket_qua(25, so_b=2))
# Output:
# [tinh_toan_ket_qua] - Bắt đầu thực thi.
# [tinh_toan_ket_qua] - Kết thúc thực thi.
# 50

Tóm tắt về Decorator

  1. Decorator trong Python là một hàm bậc cao.
  2. Nó nhận một hàm khác (hàm được trang trí) làm đối số.
  3. Nó trả về một hàm mới (thường được gọi là hàm bao bọc hoặc wrapper) thay thế hàm gốc.
  4. Cú pháp @decorator_name là một cách viết ngắn gọn (syntactic sugar) để áp dụng decorator, tương đương với việc gán ham_goc = decorator_name(ham_goc).
  5. Mục đích chính của decorator là mở rộng hoặc sửa đổi hành vi của một hàm hoặc lớp mà không thay đổi vĩnh viễn mã nguồn của nó, giúp tách biệt các mối quan tâm (separation of concerns).

Thẻ: python Decorators HigherOrderFunctions currying closures

Đăng vào ngày 27 tháng 10 lúc 13:12