Decorator là một khái niệm quan trọng trong Python, thường được sử dụng để thêm chức năng bổ sung cho hàm mà không cần sửa đổi hàm gốc.
Decorator cơ bản
Ví dụ, anh Nam mua một chiếc iPhone12 làm quà sinh nhật cho bạn gái, điện thoại còn nguyên hộp chưa mở.
def qua_tang():
print('iPhone12')
qua_tang() # Chạy để hiển thị thông tin quà tặng
Nhưng anh cảm thấy quà còn đơn giản, nên đã mua thêm một hộp sô-cô-la Dove, một thỏi son Dior, và tìm một hộp quà xinh xắn để đóng gói, bên trong đầy các viên bóng xốp.
def qua_tang():
print('iPhone12')
def hop_qua(something):
print('='*5 + 'Hộp quà' + '='*5)
print('Một hộp bóng xốp')
print('Nhiều thanh sô-cô-la')
print('Một thỏi son Dior')
return something
qua_tang = hop_qua(qua_tang) # Đóng gói quà và coi đó là quà tặng
qua_tang() # Hiển thị thông tin quà tặng
Kết quả chạy như sau:
=====Hộp quà=====
Một hộp bóng xốp
Nhiều thanh sô-cô-la
Một thỏi son Dior
iPhone12
Hàm hop_qua này chính là một decorator, tham số của nó là một đối tượng hàm, tương tự như các kiểu dữ liệu như số, chuỗi, danh sách, từ điển, hàm và lớp cũng có thể được sử dụng làm tham số hàm, vì trong Python, mọi thứ đều là đối tượng.
hop_qua khi sử dụng vẫn trả về hàm qua_tang gốc, chỉ là trước khi lấy hàm qua_tang, nó đã thêm hai bất ngờ bổ sung, sau đó chúng ta sử dụng hop_qua như là qua_tang.
Decorator về bản chất là một hàm bậc cao, lấy hàm làm tham số, thực hiện một số xử lý trên hàm đó và thay thế hàm gốc. Trong ví dụ trên, sử dụng decorator được biểu diễn như sau.
def hop_qua(something): # Decorator lấy hàm làm tham số
print('='*5 + 'Hộp quà' + '='*5)
print('Một hộp bóng xốp')
print('Nhiều thanh sô-cô-la')
print('Một thỏi son Dior')
return something
@hop_qua # Gắn decorator, sẽ tự động thay thế hàm gốc
def qua_tang():
print('iPhone12')
qua_tang() # `qua_tang` ở đây thực chất là `hop_qua(qua_tang)` đã được trang trí
Kết quả chạy giống như ví dụ trên.
Xử lý tham số hàm
Anh Nam đột nhiên nghĩ rằng, nên hỏi ý kiến bạn gái về màu sắc nào, tức là hàm qua_tang gốc nên hỗ trợ một tham số màu sắc để lựa chọn.
def qua_tang(mau_sac):
print(f'iPhone12 phiên bản {mau_sac}')
Với vai trò là một người bạn trai cẩn thận, anh Nam cần chọn bóng xốp có màu tương ứng với màu điện thoại, tức là cần có thể lấy được tham số của hàm qua_tang đã được trang trí.
Lúc này, chúng ta cần bên trong hộp (decorator hop_qua), chuẩn bị lại một món quà mới, xử lý khác nhau dựa trên tham số màu sắc, sau đó lấy món quà iPhone12 tương ứng dựa trên màu sắc.
Vì decorator hop_qua gốc trả về hàm qua_tang, chúng ta không thể sửa đổi nội dung của hàm gốc, do đó không thể điều chỉnh phù hợp với tham số của hàm.
Lúc này chúng ta có thể sử dụng "thay thế người thật bằng người giả", sử dụng một hàm tương tự qua_tang_moi để thay thế hàm gốc, nguyên tắc như sau:
- Đối tượng hàm thay thế
qua_tang_moiphải sử dụng nhất quán với hàm gốcqua_tang(tham số nhất quán) - Đối tượng hàm thay thế
qua_tang_moiphải có chức năng của hàm gốcqua_tang(có thể gọi trực tiếp hàm gốcqua_tangtrongqua_tang_moiđể thực hiện) - Decorator chuyển từ trả về đối tượng
qua_tangthành trả về đối tượngqua_tang_moi
Cách thức đại khái như sau:
def qua_tang_moi(mau_sac): # Tham số nhất quán với hàm gốc
print(f'Một hộp bóng xốp {mau_sac}') # Chuẩn bị bóng xốp theo màu
print('Nhiều thanh sô-cô-la')
print('Một thỏi son Dior')
return qua_tang(mau_sac) # Gọi hàm gốc để chứa chức năng hàm gốc # Fixme chưa định nghĩa qua_tang bên dưới
def hop_qua(something):
print('='*5 + 'Hộp quà' + '='*5)
return qua_tang_moi # Thay thế bằng hàm mới, vì hàm gốc không thể sửa đổi, hàm mới có thể tùy chỉnh và chứa chức năng hàm gốc
@hop_qua
def qua_tang(mau_sac):
print(f'iPhone12 phiên bản {mau_sac}')
qua_tang('đỏ')
Do qua_tang_moi cần sử dụng qua_tang, nên qua_tang phải được định nghĩa ở trên, và không thể sử dụng @hop_qua để trang trí (vì hop_qua chưa định nghĩa, hop_qua cần sử dụng qua_tang_moi nên chỉ có thể định nghĩa ở dưới, và sau khi trang trí @hop_qua, qua_tang sẽ trở thành `qua_tang_moi).
graph LR A[hop_qua] -->|cần trả về hàm mới| B[qua_tang_moi] B-->|cần gọi hàm gốc| C[qua_tang] C -->|cần sử dụng decorator| A > Lưu ý: qua_tang phụ thuộc vào hop_qua là sử dụng @hop_qua để trang trí qua_tang phải có định nghĩa hop_qua trước
Làm thế nào để giải quyết vấn đề này?
Vấn đề cốt lõi là qua_tang_moi làm thế nào để lấy được hàm gốc qua_tang. Chúng ta nhận thấy hop_qua thực tế không phụ thuộc vào qua_tang, nhưng có thể lấy đối tượng qua_tang thực tế thông qua tham số func (@hop_qua tức là hop_qua(qua_tang), qua_tang là tham số thực khi gọi hop_qua).
Điều kiện hiện tại có thể được tổng hợp như sau:
- Trang trí
qua_tangcần định nghĩahop_quatrước hop_quacó thể lấy đượcqua_tang,qua_tang_moicầnqua_tang,hop_quacầnqua_tang_moi
Do qua_tang được định nghĩa trước phạm vi toàn cục (mô-đun), không thể lấy được hàm gốc qua_tang, trong khi hop_qua sau khi truyền tham số thực có thể lấy được qua_tang, do đó chúng ta có thể định nghĩa qua_tang_moi trực tiếp bên trong hàm hop_qua.
Cách này vừa giải quyết vấn đề qua_tang_moi cần qua_tang, vừa giải quyết vấn đề hop_qua trả về cần qua_tang_moi.
Cách sử dụng định nghĩa một hàm bên trong một hàm khác để vượt qua giới hạn phạm vi được gọi là closure.
Mã thực hiện như sau:
def hop_qua(something):
print('='*5 + 'Hộp quà' + '='*5)
def qua_tang_moi(mau_sac): # `qua_tang_moi` ở trong `hop_qua`, có thể lấy được tham số trong `hop_qua`
print(f'Một hộp bóng xốp {mau_sac}')
print('Nhiều thanh sô-cô-la')
print('Một thỏi son Dior')
return something(mau_sac) # func thực chất là hàm `qua_tang`
return qua_tang_moi # Trả về quà mới, khi gọi quà mới, thêm một số bất ngờ và trả về kết quả của quà gốc `qua_tang(mau_sac)`.
@hop_qua
def qua_tang(mau_sac):
print(f'iPhone12 phiên bản {mau_sac}')
qua_tang('đỏ') # Thực chất `qua_tang` ở đây là hàm `qua_tang_moi` đã được `hop_qua` trang trí, còn `qua_tang_moi('đỏ')` trả về kết quả của `qua_tang('đỏ')`.
Trong decorator hop_qua, để xử lý tương ứng dựa trên tham số, chúng ta đã tạo một hàm mới, hàm bên trong cũng có thể định nghĩa hàm bên trong, hàm bên trong qua_tang_moi có thể lấy và sử dụng tham số của hàm bên ngoài hop_qua, như qua_tang.
Để có thể lấy được tham số của hàm gốc qua_tang, chúng ta cần xây dựng một hàm giả qua_tang_moi, hàm này có tham số nhất quán, kết quả trả về nhất quán với hàm gốc qua_tang, tức là qua_tang_moi('đỏ') trả về chính là qua_tang('đỏ').
Sau đó "thay thế người thật bằng người giả", không còn trả về đối tượng hàm gốc qua_tang, mà trả về đối tượng hàm thay thế qua_tang_moi.
Chạy sau khi hiển thị
=====Hộp quà=====
Một hộp bóng xốp đỏ
Nhiều thanh sô-cô-la
Một thỏi son Dior
iPhone12 phiên bản đỏ
Lưu ý: Trong decorator
hop_qua, cần trả về một đối tượng hàm, như ví dụ trênreturn qua_tanghoặc ví dụ nàyreturn qua_tang_moi. Trong hàm giảqua_tang_moi, để kết quả nhất quán với hàm gốcqua_tang, cần trả về kết quả gọi hàm gốc, tức làqua_tang(mau_sac).
Về mặt phổ biến, với tư cách là người bán hàng, để decorator hop_qua có thể đóng gói bất kỳ loại quà nào, bất kể quà có tham số gì cũng có thể đáp ứng, điều này đòi hỏi hàm giả qua_tang_moi của chúng ta phải hỗ trợ bất kỳ loại tham số nào, tức là def qua_tang_moi(*args, **kwargs).
Sau đó, truyền mọi tham số *args, **kwargs cho hàm gốc qua_tang(*args, **kwargs) xử lý.
Sau khi sửa đổi, chúng ta sẽ có một decorator chung có thể đóng gói bất kỳ quà nào.
def hop_qua(something):
print('='*5 + 'Hộp quà' + '='*5)
def qua_tang_moi(*args, **kwargs): # Chấp nhận số lượng tham số bất kỳ
if args and len(args) > 0: # Vì tham số không xác định, chúng ta giả sử nếu có tham số, tham số đầu tiên là tham số màu sắc
mau_sac = args[0]
print(f'Một hộp bóng xốp {mau_sac}')
else:
print(f'Một hộp bóng xốp')
print('Nhiều thanh sô-cô-la')
print('Một thỏi son Dior')
result = something(*args, **kwargs) # Nếu chúng ta cần xử lý kết quả của hàm gốc, có thể lấy kết quả trước
# print(f'Kết quả hàm gốc {result}') Vì hàm gốc `qua_tang` không có `return`, đây thực chất là `None`
return result # Trả về kết quả hàm gốc
return qua_tang_moi
@hop_qua
def qua_tang(mau_sac, pro=False): # Hàm quà mới, hai tham số, mặc định mua 12, nếu bạn gái muốn Pro thì cũng được
if pro is True:
print(f'iPhone12 Pro phiên bản {mau_sac}')
else:
print(f'iPhone12 phiên bản {mau_sac}')
qua_tang('xanh dương biển', pro=True)
Như vậy, dù hàm được trang trí có bao nhiêu tham số, decorator hop_qua cũng có thể xử lý bình thường.
Sau khi chạy, hiển thị như sau.
=====Hộp quà=====
Một hộp bóng xốp xanh dương biển
Nhiều thanh sô-cô-la
Một thỏi son Dior
iPhone12 Pro phiên bản xanh dương biển
Decorator có tham số
Anh Nam tự tin cảm thấy, có thể thêm một vài chi tiết vào hộp quà, chọn các hình dạng hộp khác nhau theo sở thích của bạn gái, do đó chúng ta cần dựa trên tham số để tùy chỉnh decorator hop_qua của mình, thêm một lớp hàm tùy chỉnh bên ngoài hộp.
def hop_qua_tuy_chinh(hinh_dang): # Tùy chỉnh decorator dựa trên tham số
def hop_qua(something): # Hàm decorator thực tế ---------------------------
print('='*5 + f'Hộp quà {hinh_dang}' + '='*5) # Tùy chỉnh theo hình dạng
# ...
return hop_qua # Trả về hàm decorator đã tùy chỉnh
Lúc này chúng ta có một hàm decorator có thể tùy chỉnh dựa trên tham số hop_qua_tuy_chinh, decorator này nhận được tham số sẽ truyền cho decorator thực tế hop_qua, và trả về hàm decorator hop_qua đã tùy chỉnh.
Mã đầy đủ như sau.
def hop_qua_tuy_chinh(hinh_dang): # Tùy chỉnh decorator dựa trên tham số =====================
def hop_qua(something): # Hàm decorator thực tế ---------------------------
print('='*5 + f'Hộp quà {hinh_dang}' + '='*5)
def qua_tang_moi(*args, **kwargs): # Hàm giả ..............
if args and len(args) > 0:
mau_sac = args[0]
print(f'Một hộp bóng xốp {mau_sac}')
else:
print(f'Một hộp bóng xốp')
print('Nhiều thanh sô-cô-la')
print('Một thỏi son Dior')
result = something(*args, **kwargs)
return result # Trả về kết quả hàm gốc ......................
return qua_tang_moi # Trả về hàm giả ---------------------------
return hop_qua # Trả về decorator đã tùy chỉnh ===============================
@hop_qua_tuy_chinh('trái tim') # Sử dụng decorator có thể tùy chỉnh
def qua_tang(mau_sac, pro=False):
if pro is True:
print(f'iPhone12 Pro phiên bản {mau_sac}')
else:
print(f'iPhone12 phiên bản {mau_sac}')
qua_tang('xanh dương biển', pro=True)
Lưu ý: Decorator được tính ngay khi nhập mô-đun, tức là trước khi gọi
qua_tang('xanh dương biển', pro=True)đã thực thi tạo rahop_quađã tùy chỉnh.
Sau khi chạy, kết quả như sau.
=====Hộp quà trái tim=====
Một hộp bóng xốp xanh dương biển
Nhiều thanh sô-cô-la
Một thỏi son Dior
iPhone12 Pro phiên bản xanh dương biển