Decorator là một trong những design pattern phổ biến trong lập trình. Nó thật sự hữu dụng và linh hoạt trong quá trình phát triển phần mềm. Trong bài viết này, mình sẽ chia sẻ cách hoạt động và sử dụng nó trong python.
Decorator là một pattern trong đó một hàm (class) sẽ nhận input là một hàm (class) và trả ra output là một hàm (class). Hàm được trả ra sẽ có hành vi khác so với hàm input, có thể được extends,...
OK, mình sẽ lấy ví dụ trực quan hơn
Giả sử mình có hàm tính tổng như sau:
def add(*args):
return sum(args)
Bây giờ, mình có thêm yêu cầu là log thời gian thực thi hàm đó chẳng hạn, cách dễ nhất là thêm code vào chính hàm đó như sau:
def add(*args):
start_at = time.time()
s = sum(*args)
print("Take", time.time()-s, "second")
return s
Bây giờ, mình có thêm mấy hàm và cũng được yêu cầu log thời gian thực thi. Sau 2 tháng, các sếp yêu cầu mình ngoài log thời gian thực thi, cần log thêm tên hàm và input chẳng hạn, mình sẽ phải sử code ở toàn bộ các hàm đó, thật tệ :sad:
Đây là lúc chúng ta cần đến decorator
def log(func):
def func_with_log(*args, **kwargs):
start_at = time.time()
ret = func(*args, **kwargs)
print("Take", time.time()-start_at, "second")
return ret
return func_with_log
Sử dụng nó nào
def add(*args):
return sum(args)
add_with_log = log(add)
Hàm log
đơn giản nhận vào một hàm, thực thi hàm đó (pass nguyên bộ tham số) và in thời gian thực thi (extend hàm gốc). Bạn có thể sử dụng hàm log
này ở đâu bạn muốn và có thể dễ dàng thêm các yêu cầu mà không tốn công :smile:
Ngoài cách gọi rõ ràng như thế kia, Python còn hỗ trợ syntax cho decorator như này
@log
def add(*args):
return sum(args)
Amazing!
Chúng ta sẽ thắc mắc, một hàm có thể có nhiều decorator hay không, câu trả lời là có
Chúng được gọi là decorator stack
(các bạn tự tìm hiểu thêm nhé :smile:)
Hay cũng xét ví dụ sau
def decorator1(func):
def new_func(*args, **kwargs):
print('Decorator 1')
return func(*args, **kwargs)
return new_func
def decorator2(func):
def new_func(*args, **kwargs):
print('Decorator 2')
return func(*args, **kwargs)
return new_func
@decorator1
@decorator2
def func(msg):
print('Original func:', msg)
func("Hello world")
Output sẽ là:
Decorator 1
Decorator 2
Original func: Hello world
Oh, vậy chúng có thứ tự thực hiện như thế nào?
Các decorator lồng nhau theo thứ tự decorator1 -> decorator2.
Còn thứ tự thực thi sẽ tuỳ vào việc bạn gọi func
trong các decorator ra sao.
Đây chính xác là những gì syntax @
là với các decorator
def func(msg):
print("Original func:", msg)
func_with_deco2 = decorator2(func)
func_with_deco1 = decorator1(func)
Vẫn là ví dụ về log ở phần 1, chúng ta giờ muốn thêm log tên hàm và input của hàm đó. Nhưng log input của hàm là một tuỳ chọn có thể log hoặc không log
Giải pháp 1: viết 2 decorator
def log_with_input(func):
def func_with_log(*args, **kwargs):
start_at = time.time()
ret = func(*args, **kwargs)
print(func.__name__, "takes", time.time()-start_at, "second")
print("Input:", args, kwargs)
return ret
return func_with_log
def log_without_input(func):
def func_with_log(*args, **kwargs):
start_at = time.time()
ret = func(*args, **kwargs)
print(func.__name__, "takes", time.time()-start_at, "second")
return ret
return func_with_log
Giờ đâu cần log input thì dùng log_with_input
, nếu không thì xài log_without_input
.
Nếu bạn có n biến là tuỳ chọn thì bạn tiêu rồi, bạn cần viết 2 ^ n cái decorator
OK, mình có giải pháp cho bạn đây: decorator của decorator.
def log(show_input=False):
def config(func):
def func_with_log(*args, **kwargs):
start_at = time.time()
ret = func(*args, **kwargs)
print(func.__name__, "takes", time.time()-start_at, "second")
if show_input:
print("Input:", args, kwargs)
return ret
return func_with_log
return config
và đơn giản chỉ cần sử dụng như
@log(show_input=True)
def need_log_input():
...
@log(show_input=False)
def dont_need_log_input():
...
OK, mình sẽ giải thích thằng Python nó làm ngầm chuyện này như thế nào nhé :smile:
log_deco = log(show_input=True) # cái này trả về hàm decorator đã được config: show_input=True
need_log_input = log_deco(need_log_input)
Bạn có thể thấy, hàm config
nhận hàm ban đầu và trả ra một hàm mới là func_with_log
có chứa đoạn code log thông tin, và thực thi hàm gốc.
Và hàm func_with_log
dựa vào biến show_input
để quyết định xem có nên log ra input của hàm gốc hay không?
Đây là một lợi ích đến từ nested function
. Việc define các hàm lồng nhau sẽ cho chúng ta lợi ích variable scope
. Tức là sau khi hàm config
trả về func_with_log
, biến show_input
vẫn tồn tại trong hàm func_with_config
mà không bị giải phóng :smile:, lí do tại sao mình sẽ giải thích trong bài viết sau nhé.
OK, các bạn có thể hiểu rõ cơ chế làm việc của decorator rồi, nhưng Python có hẳn syntax cho pattern này
Nhanh gọn phải không nào :smile:
Các bạn có thể thấy pattern này được sử dụng trong rất nhiều các framework như Flask chẳng hạn
app = Flask(__name__)
@app.route("/")
def homepage():
return "Hello world"
Nếu các bạn để ý, sẽ gặp trường hợp này:
from functools import lru_cache
@lru_cache
def f1():
...
@lru_cache(max_size=1024)
def f2():
...
Tại sao chỗ thì cần (max_size=1024)
chỗ lại không? Vậy hãy đến phần nâng cao hơn nhé :smile:
Quay lại ví dụ ở phần trước, show_input
là một tuỳ chọn, giả sử nếu không truyền vào ta có thể hiểu nó là False
, và mình cũng muốn sử dụng nó như lru_cache
ở trên :smile:
def log(func=None, show_input=None):
if func is None: # lúc gọi @log(show_input=True)
return lambda origin_func: log(origin_func, show_input=show_input)
show_input = False # giá trị mặc định
def func_with_log(*args, **kwargs):
start_at = time.time()
ret = func(*args, **kwargs)
print(func.__name__, "takes", time.time()-start_at, "second")
if show_input:
print("Input:", args, kwargs)
return ret
return func_with_log
Cùng phân tích nhé
Với trường hợp bạn gọi @log
, sẽ tương đương như sau:
new_func = log(old_func)
nó sẽ lấy giá trị mặc định là false
Với trường hợp gọi @log(show_input=True)
, sẽ tương đương:
deco = log(show_input=True). # deco đã được config
new_func = deco(old_func)
Vậy là vấn đề đã được giả quyết :smile:
Tóm lại, bằng việc sử dụng decorator, code của bạn sẽ:
Đến đây mình đã chia sẻ khái niệm decorator nói chung và trong Python nói riêng, hi vọng bài viết này hữu dụng với các bạn, bye bye.