Để 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:
- Nhận một hoặc nhiều hàm khác làm đối số.
- 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
- Decorator trong Python là một hàm bậc cao.
- Nó nhận một hàm khác (hàm được trang trí) làm đối số.
- 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.
- 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).
- 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).