Decorator là một tính năng thú vị và mạnh mẽ trong Python. Nó cho phép ta tuỳ ý chỉnh sửa, thêm chức năng của một hàm có sẵn mà không cần sửa code của hàm, giúp cho việc maintain và scale tốt hơn. Vậy cụ thể decorator là gì và làm gì?
Đọc lý thuyết thì hơi khó, nên mình sẽ lấy một vài ví dụ cho các bạn dễ hình dung. Tuy nhiên trước tiên mình sẽ điểm qua một vài tính năng quan trọng mà Python cung cấp.
Bạn có thể gán một hàm vào biến
def hello(name): return "Hello " + name greet = hello print(greet("Hung"))
> Hello Hung
Bạn có thể khai báo hàm trong một hàm khác
def plus_one(x): def increase(y): return y + 1 return increase(x) print(plus_one(19))
> 20
Một hàm có thể trả về một hàm
def plus_one(): def increase(y): return y + 1 return increase a = plus_one() print(a(10))
> 11
Tham số của một hàm có thể là một hàm
def calculate(f, x, y): return f(x,y) def add(a, b): return a+b sum = calculate(add, 10, 11) print(sum)
> 21
Inner function có thể truy cập được enclosing scope
Cái này còn gọi là closure, có thể hiểu đơn giản qua đoạn code sau
def say_hello(name): def greet(): return "Hello " + name return greet() print(say_hello("Hung"))
> Hello Hung
Bạn có thể thấy trong hàm greet không hề có biến name
, nhưng nó vẫn có quyền truy cập biến name
Decorator là gì
Hiểu một cách đơn giản, decorator là một wrapper cho một hàm sẵn có. Xét ví dụ (a) sau
def author_of_huwng(): return "Nguyen Viet Hung" def first_decorator(f): return f() + "\nDecorator has been used." a = first_decorator(author_of_huwng) # This is a string print(a)
> Nguyen Viet Hung
> Decorator has been used.
Chúng ta hãy tối ưu đoạn code ở trên một chút. (ví dụ (b))
def first_decorator(f): def wrap(): return f() + "\nDecorator has been used." return wrap def author_of_huwng(): return "Nguyen Viet Hung" a = first_decorator(author_of_huwng) # This is a function print(a())
> Nguyen Viet Hung
> Decorator has been used.
Chúc mừng bạn. Bạn vừa cài đặt decorator đầu tiên. Chúng ta hãy nhìn xem ví dụ (a) và (b) khác nhau gì nhé.
Bạn hãy nhìn vào kiểu trả về của hàm first_decorator
ở hai ví dụ. Ở ví dụ (a) first_decorator
trả về kiểu string, ví dụ (b) first_decorator
trả về một hàm số. Cả hai ví dụ này bạn đều đã sử dụng decorator. Nhưng nói chung trong thực hành cách ở ví dụ (b) sẽ được sử dụng nhiều hơn vì decorator này nhận vào một hàm và trả lại một hàm.
Python cung cấp một syntatic sugar (syntatic sugar hiểu nôm na là một cú pháp tương đương để viết code dễ đọc hơn) cho decorator. Đoạn code ở ví dụ (b) tương đương với đoạn code sau đây.
def first_decorator(f): def wrap(): return f() + "\nDecorator has been used." return wrap @first_decorator def author_of_huwng(): return "Nguyen Viet Hung" a = author_of_huwng() print(a)
> Nguyen Viet Hung
> Decorator has been used.
Thay vì phải gọi first_decorator(author_of_huwng)
bạn sẽ sử dụng cú pháp @first_decorator
bên trên hàm author_of_huwng
. Nó sẽ làm công việc tương tự như ví dụ (b) nhưng hàm author_of_huwng
đã được biến thành hàm first_decorator(author_of_huwng)
(Python đổi tên cho mình). Đây là một tính năng của Python khiến việc sử dụng decorator được trong sáng, dễ đọc hơn.
Ví dụ (a) cũng có thể viết lại để sử dụng syntatic sugar như ví dụ (b). (Tuy nhiên như mình nói ở trước decorator thường được trả về là một hàm số chứ không phải một string)
def first_decorator(f): return f() + "\nDecorator has been used." @first_decorator def author_of_huwng(): return "Nguyen Viet Hung" a = author_of_huwng # This is a string print(a)
> Nguyen Viet Hung
> Decorator has been used.
Cái gì có thể sử dụng làm decorator
Như bạn đã thấy ở trên thì một hàm số có thể sử dụng làm một decorator. Vậy ngoài hàm số thì những cái gì có thể sử dụng như làm decorator. Thực tế, bất cứ cái gì callable (callable: có thể gọi được, ví dụ như print
là callable còn một string name="hung"
thì không callabe) đều có thể sử dụng làm decorator. Ngoài hàm được sử dụng phần lớn để làm decorator, Class và Instance là hai cấu trúc cũng được sử dụng phổ biến.
Class Decorator
Hãy xét các ví dụ sau:
class ClassCount: def __init__(self, f): self.f = f self.count =0 def __call__(self, *args, **kwargs): self.count += 1 return self.f(*args, **kwargs) @ClassCount def hello(name): print("Hello, {}".format(name)) hello("Hung") hello("Thanh") hello("Kien") hello("Trung") hello("Quynh") hello("Dat") hello.count # This should return 6
> Hello, Hung
> Hello, Thanh
> Hello, Kien
> Hello, Trung
> Hello, Quynh
> Hello, Dat
> 6
Trong ví dụ trên, class ClassCount
đã được sử dụng là một decorator. Vì class ClassCount
là callable (do chúng ta có hàm __call__
nên khi gọi ClassCount(f)
thì tương đương với việc chúng ta gọi hàm __call__(f))
. Khi đó, hàm số hello
của chúng ta trở thành một instance của class ClassCount
và do đó, trong ví dụ này chúng ta sẽ theo dõi được số lần gọi hàm hello
(6
).
Instance Decorator
Ngoài class, instance cũng có thể sử dụng làm decorator được. Xét ví dụ dưới đây.
class Trace: def __init__(self): self.enabled = True def __call__(self, f): def wrap(*args, ** kwargs): if self.enabled: print("Calling {}". format(f)) return f(*args, **kwargs) return wrap tracer = Trace() @tracer def generator_random(): from random import randint return randint(0, 100000) print(generator_random()) print(generator_random()) print(generator_random())
Calling
78101
Calling
61074
Calling
356
Tương tự như Class, Instance cũng có thể sử dụng làm decorator được. Khi truyền hàm vào instance decorator, nó cũng gọi hàm __call__ và trả lại cho ta một hàm. Instance đặc biệt hữu dụng khi muốn tạo ra các hàm số mà chúng ta có thể linh hoạt điều khiển chúng được. Cụ thể trong ví dụ trên, nếu chúng ta không muốn xem hàm nào được gọi nữa. Chúng ta có thể tắt nó đi như sau.
new_tracer = Trace() new_tracer.enableenable = False @new_tracer def generator_random(): from random import randint return randint(0, 100000) print(generator_random()) print(generator_random()) print(generator_random())
> 56449
> 65754
> 59666
Decorating methods
Method cũng có thể sử dụng làm một decorator được. Hãy xét ví dụ sau
class CityMaker: def __init__(self, suffix): self.suffix = suffix @tracer def add_city(self, name): print(name + self.suffix) a = CityMaker(" city") a.add_city("Ha Long")
Multiple decorator
Các ví dụ trên chúng ta đều sử dụng một decorator duy nhất. Tuy nhiên chúng ta có thể sử dụng nhiều decorator kết hợp với nhau với lưu ý là sắp xếp theo thứ tự ngược lại. Xét ví dụ:
@decorator2 @decorator1 def foo()
Ở ví dụ này, decorator1 sẽ được áp dụng vào hàm foo, sau đó hàm trả về sẽ là tham số truyền vào decorator2. Cụ thể là decorator2(decorator1(foo))
class Trace: def __init__(self): self.enabled = True def __call__(self, f): def wrap(*args, ** kwargs): if self.enabled: print("Calling {}". format(f)) return f(*args, **kwargs) return wrap tracer = Trace() def add_city(f): def wrap(*args, **kwargs): x = f(*args, **kwargs) return x + " city" return wrap @tracer @add_city def home_town(city): return city home_town("Ha Long")
Calling
‘Ha Long city’
functools.wraps để bảo toàn metadata
Xét ví dụ sau:
def say_oh_yeah(): """Say M-TP's famous quote""" return "Oh Yeah!" print(say_oh_yeah.__name__) print(say_oh_yeah.__doc__)
> say_oh_yeah
> Say M-TP's famous quote
Bây giờ hãy bọc hàm say_oh_yeah vào một decorator như ví dụ sau:
def dec(f): """A decorator""" def wrapper(): """A wrapper""" return f return wrapper @dec def say_oh_yeah(): """Say M-TP's famous quote""" return "Oh Yeah!" print(say_oh_yeah.__name__) print(say_oh_yeah.__doc__)
> wrapper
> A wrapper
Thật bất ngờ, các thông tin của hàm say_oh_yeah đã biến mất, bao gồm tên hàm mà docstring của hàm.
Để giải quyết vấn đề này, ta có thể làm như sau:
def dec(f): """A decorator""" def wrapper(): """A wrapper""" return f wrapper.__name__ = f.__name__ wrapper.__doc__ = f.__doc__ return wrapper @dec def say_oh_yeah(): """Say M-TP's famous quote""" return "Oh Yeah!" print(say_oh_yeah.__name__) print(say_oh_yeah.__doc__)
> say_oh_yeah
> Say M-TP's famous quote
Tuy nhiên, cách này hơi rườm rà, thay vào đó, chúng ta có thể sử dụng functools
như một decorator như sau:
import functools def dec(f): """A decorator""" @functools.wraps(f) def wrapper(): """A wrapper""" return f return wrapper @dec def say_oh_yeah(): """Say M-TP's famous quote""" return "Oh Yeah!" print(say_oh_yeah.__name__) print(say_oh_yeah.__doc__)
> say_oh_yeah
> Say M-TP's famous quote
Kết quả là name và docstring của hàm say_oh_yeah
vẫn giữ nguyên, không bị ảnh hưởng và functools
đã giúp chúng ta giải quyết vấn đề này một cách nhanh chóng.
Kiểm tra tham số
Decorator có thể sử dụng để kiểm tra điều kiện của tham số. Giả sử ta một tạo một hàm create(value, size)
với điều kiện size >=0
. Ta có thể sử dụng decorator để giải quyết bài toán như sau:
def check_non_negative(index): def validator(f): def wrap(*args): if args[index] < 0: raise ValueError("Argument {} must be non-negative".format(index)) return f(*args) return wrap return validator @check_non_negative(1) def create_list(value, size): return [value] * size
create_list('a', 10)
> ['a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a']
create_list('Hung', 0)
> []
create_list("M-TP", -3)
—————————————————————————
ValueError Traceback (most recent call last)
in ()
—-> 1 create_list(“M-TP”, -3)in wrap(*args)
4 if args[index] < 0: 5 raise ValueError( —-> 6 “Argument {} must be non-negative”.format(index))
7 return f(*args)
8 return wrapValueError: Argument 1 must be non-negative
Lời kết
Trong bài viết bạn đã được giới thiệu khái niệm về decorator và một số ví dụ về cách dùng cơ bản. Đây là một pattern quan trọng của Python, cung cấp cho chúng ta một cách để thao tác, chỉnh sửa với các hàm sẵn có mà không cần phải trực tiếp tác động vào hàm đó, giúp giảm thiểu lỗi và khả năng scale tốt hơn. Hi vọng bài viết sẽ giúp các bạn phần nào hiểu thêm về pattern decorator trong Python và hãy sử dụng decorator khi viết chương trình Python tiếp theo.