Decorator in short

qxf2-gun-decorator1
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à namedocstring 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 wrap

ValueError: 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.

Trả lời

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Đăng xuất /  Thay đổi )

Google photo

Bạn đang bình luận bằng tài khoản Google Đăng xuất /  Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Đăng xuất /  Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Đăng xuất /  Thay đổi )

Connecting to %s