Tìm hiểu Python decorators

Tìm hiểu Python decorators
Photo by Uwe Baumann from Pixabay

Decorator trong Python được sử dụng khá nhiều, tuy nhiên nó cũng hơi khó hiểu một chút. Decorator cho chúng ta một cú pháp đơn giản để gọi các hàm bậc cao (higher-order functions). Về mặt lý thuyết, một decorator là một hàm nhận tham số đầu vào là một hàm khác và mở rộng tính năng cho hàm đó mà không thay đổi nội dung của nó. Tuy nhiên, decorator không chỉ có vậy, nó thực sự còn làm nhiều việc khác nữa. Chúng ta sẽ dần dần khám phá trong bài viết này.

Trong bài viết này, tôi sẽ giới thiệu một cách ngắn gọn và dễ hiểu về decorator, cách sử dụng chúng và cách tự viết một decorator cho riêng mình.

Từ “decorator” được sử dụng trong Python có một chút biến đổi. Bạn hãy cẩn thận vì nó rất dễ gây nhầm lẫn với một design pattern cũng tên là decorator. Thực ra, chúng ta có thể sử dụng decorator trong Python để cài đặt decorator pattern. Nhưng có rất nhiều hạn chế trong cách làm này. Theo tôi thấy thì decorator của Python có nhiều nét giống với macro hơn.

Cơ sở lý thuyết

Trước khi bạn có thể hiểu các decorator, thì bạn cần hiểu một số khái niệm cơ bản của Python trước, trong phần này, chúng ta sẽ dần dần tìm hiểu những khái niệm đó.

Hàm

Hàm là một khái niệm rất cơ bản trong lập trình nói chung và Python nói riêng, nên tôi nghĩ không cần nói nhiều về nó. Đương nhiên các hàm sẽ trả về kết quả dựa trên tham số đầu vào của nó:

>>> def greet(word="Hello"):
...     print(word)
...
>>> greet()
Hello
>>> greet("Hello, world")
Hello, world
>>>

Tôi nghĩ không cần phải nói nhiều về khái niệm này nữa, nó rất dễ hiểu. Nên chúng ta sẽ chuyển sang khái niệm tiếp theo.

Hàm cũng là đối tượng

Trong Python, hàm cũng là đối tượng, hơn nữa nó còn là đối tượng first-class. Điều đó cho rất nhiều kết quả quan trọng. Chúng ta sẽ tìm hiểu thông qua một số ví dụ sau:

def shout(word="aaa"):
    return word.upper() + "!"

print(shout()) # output: AAA!

# Bởi vì hàm cũng là đối tượng nên chúng ta có thể gán nó cho các
# biến như những đối tượng khác.
scream = shout

# Như bạn đã thấy rằng, chúng ta không dùng dấu ngoặc, bởi vì
# chúng ta không gọi hàm, mà chúng ta đang gán hàm cho biến khác
# (scream).  Sau khi gán, chúng ta có thể gọi hàm shout từ biến
# scream.
print(scream()) # output: AAA!

# Không chỉ gán biến, chúng ta còn có thể xoá hàm như xoá những
# đối tượng thông thường khác.
del shout
try:
    print(shout())
except NameError as e:
    print(e)
# output: name 'shout' is not defined

# Tuy nhiên, chúng ta vẫn có thể gọi hàm thông qua biến scream.
print(scream()) # output: AAA!

Các hàm lồng nhau

Vì hàm là các đối tượng first-class (thực ra trong Python, mọi thứ đều là đối tượng first-class) nên nó có một đặc tính khá thú vị là nó có thể được định nghĩa bên trong một hàm khác. Các hàm như vậy gọi là các hàm lồng nhau.

def talk():

    # Bạn có thể định nghĩa các hàm bên trong hàm `talk`
    def whisper(word="aaa"):
        return word.lower() + "..."

    # Và sau đó sử dụng hàm này
    print(whisper())

# Bạn có thể gọi hàm talk, mỗi lần bạn gọi hàm này, hàm whisper
# sẽ được định nghĩa sau đó thực thi ở bên trong hàm talk.
talk() # output: aaa...

# Nhưng bạn không thể gọi hàm whisper từ bên ngoài.
try:
    print(whisper())
except NameError as e:
    print(e)
# output: name 'whisper' is not defined

Bài học rút ra

Hàm là các đối tượng (hơn nữa là đối tượng first-class), vì vậy:

  • Hàm có thể được gán cho các biến.
  • Hàm có thể được định nghĩa trong một hàm khác.

Vì những đặc điểm trên hàm còn có thể trả kết quả (return) là một hàm khác.

Một hàm trả kết quả là một hàm khác

Python cho phép bạn viết một hàm mà hàm đó trả kết quả về là một hàm khác. Ví dụ:

def get_talk(kind="shout"):

    # Trước hết, chúng ta định nghĩa các hàm
    def shout(word="aaa"):
        return word.upper() + "!"

    def whisper(word="aaa"):
        return word.lower() + "..."

    # Bây giờ, chúng ta sẽ trả kết quả về những hàm này
    if kind == "shout":
        # Chúng ta không sử dụng dấu ngoặc (), bởi vì chúng ta
        # không gọi những hàm này, mà chúng ta return đối tượng
        # tương ứng với các hàm.
        return shout
    else:
        return whisper

# Gọi những thứ trên như thế nào?

# Chúng ta gọi hàm và gán kết quả cho một biến
talk = get_talk()

# Bạn có thể thấy talk bây giờ là một đối tượng hàm
print(talk) # output: <function get_talk.<locals>.shout at 0x101bdbae8>

# Đối tượng hàm này là một trong hai hàm con của get_talk
print(talk()) # output: AAA!

# Bạn có thể dùng cách gọi ngắn gọn hơn nếu đã thành thạo
print(get_talk("whisper")()) # output: aaa...

Không chỉ có thể trả kết quả là một hàm, hàm còn thể được truyền vào hàm khác như là một tham số.

def do_smth_before(func):
    print("Something before calling function")
    print(func())

print(scream)
# output:
# Something before calling function
# AAA!

Trên đây là những thứ cần thiết để hiểu về decorator của Python. Bạn sẽ thấy rằng, decorator là “wrapper” cho phép bạn chạy một số đoạn code trước hoặc sau hàm chính mà không thay đổi nội dung của nó.

Python decorator cơ bản

Khi có một hàm mà bạn muốn chạy một số code trước, hoặc sau hàm đó, bạn sẽ làm thế nào?

Làm bằng tay

Chúng ta có thể sử dụng kiến thức về các hàm lồng nhau ở trên, để giải quyết bài toán này.

# Decorator là hàm nhận hàm khác làm tham số
def decorator_function(function_to_decorate):

    # Bên trong, chúng ta sẽ định nghĩa một "wrapper".  Hàm này sẽ
    # bao bọc xung quanh hàm gốc, vì thế chúng ta có thể chạy
    # code trước hoặc sau hàm gốc đó
    def wrapper_to_original_function():
        # Đây là code được thực thi TRƯỚC hàm gốc
        print("Before calling function")

        # Gọi hàm gốc, đây là gọi và thực thi hàm nên chúng ta sẽ
        # dùng dấu ngoặc
        function_to_decorate()

        # Đây là code được thực thi SAU hàm gốc
        print("After calling function")

    # Vào thời điểm này, hàm gốc vẫn CHƯA thực sự được thực thi,
    # chúng ta sẽ return hàm wrapper mà chúng ta vừa tạo.  Hàm này
    # chứa code được thực thi trước và sau hàm gốc, đặc biệt là
    # nó có thể được thực thi.
    return wrapper_to_original_function

# Giả sử bạn có một hàm mà không muốn ai thay đổi nó
def a_function():
    print("Do not modify it")

a_function() # output: Do not modify it

# Bây giờ, bạn muốn mở rộng hành vi của hàm này, chạy một số code
# trước và sau nó, hãy sử dụng decorator_function.  Truyền hàm này
# như là tham số của hàm decorator, nó sẽ tự động bao bọc hàm, và
# trả kết quả về là một hàm khác bạn có thể thực thi
a_decorated_function = decorator_function(a_function)

# Có thể bạn sẽ muốn chạy hàm a_decorated_function mỗi khi thực
# thi hàm a_function.  Rất đơn giản, bạn chỉ cần override nó bằng
# hàm trả về của decorator_function
a_function = decorator_function(a_function)

a_function()
# output:
# Before calling function
# Do not modify it
# After calling function

# Đây chính là những gì mà decorator có thể làm

Decorator

Chúng có thể sử dụng cú pháp decorator của Python với ví dụ trên như sau:

@decorator_function
def a_function():
    print("Do not modify it")

a_function()
# output:
# Before calling function
# Do not modify it
# After calling function

Rất dễ hiểu phải không? Cú pháp @decorator (còn gọi là pie syntax) là cách viết ngắn gọn của:

a_function = decorator_function(a_function)

Thậm chí, bạn có thể sử dụng nhiều decorator cho một hàm, bởi kết quả trả về của decorator cũng là một hàm và nó hoàn toàn có thể được decorate bởi những decorator khác.

def decorator1(func):
    def wrapper():
        print("*" * 10)
        func()
        print("*" * 10)
    return wrapper

def decorator2(func):
    def wrapper():
        print("=" * 10)
        func()
        print("=" * 10)
    return wrapper

def some_func():
    print("hello")

some_func() # output: hello
some_func = decorator1(decorator2(some_func))
some_func()
# output:
# **********
# ==========
# hello
# ==========
# **********

Sử dụng cú pháp decorator của Python

@decorator1
@decorator2
def some_func():
    print("hello")

some_func()
# output giống như lúc trước
# **********
# ==========
# hello
# ==========
# **********

# Thứ tự của decorator cũng CÓ ảnh hưởng đến kết quả
@decorator2
@decorator1
def some_func():
    print("hello")
some_func()
# output lần này đã khác
# ==========
# **********
# hello
# **********
# ==========

Python decorator nâng cao

Truyền tham số vào hàm được decorate

# Không có bí mật gì ở đây.  Chúng ta chỉ cần cho hàm wrapper nhận
# tham số là được

def a_decorator_passing_arguments(function_to_decorate):
    def a_wrapper_accepting_arguments(arg1, arg2):
        print("I got args!", arg1, arg2)
        function_to_decorate(arg1, arg2)
    return a_wrapper_accepting_arguments

# Bởi vì khi chúng ta gọi hàm, thực ra là chúng ta gọi hàm được
# trả về bởi decorator (hàm wrapper), nên nếu chúng ta truyền
# tham số cho hàm này thì nó sẽ truyền cho hàm được decorate.

@a_decorator_passing_arguments
def print_full_name(first_name, last_name):
    print("My name is", first_name, last_name)

print_full_name("naa", "manhhomienbienthuy")
# outputs:
# I got args! naa manhhomienbienthuy
# My name is naa manhhomienbienthuy

Decorate một phương thức

Hàm và phương thức của Python rất giống nhau, ngoại trừ một điểm, phương thức thuộc về một class mà nó luôn luôn có tham số đầu tiên là self (đối tượng hiện tại).

Vì vậy, chúng ta có thể viết decorator cho phương thức giống như decorator cho hàm, chỉ với lưu ý rằng cần truyền tham số self cho wrapper.

def method_friendly_decorator(method_to_decorate):
    def wrapper(self, lie):
        lie = lie - 3 # very friendly, decrease age even more :-)
        return method_to_decorate(self, lie)
    return wrapper


class Wife(object):

    def __init__(self):
        self.age = 24

    @method_friendly_decorator
    def say_your_age(self, lie):
        print("I am %s, what did you think?" % (self.age + lie))

vp = Wife()
vp.say_your_age(-3)
# outputs: I am 18, what did you think?

Decorator tổng quát

Nếu bạn muốn xây dựng một decorator một cách tổng quát, có thể áp dụng cho mọi hàm với số lượng tham số khác nhau. Bạn chỉ cần sử dụng cú pháp *args**kwargs

def decorator_passing_arbitrary_arguments(function_to_decorate):
    # Wrapper nhận mọi tham số đầu vào
    def wrapper_accepting_arbitrary_arguments(*args, **kwargs):
        print("Do I have args?:")
        print(args)
        print(kwargs)
        # Bây giờ truyền những tham số này cho hàm được decorate,
        # nhớ unpack chúng.
        function_to_decorate(*args, **kwargs)
    return wrapper_accepting_arbitrary_arguments


@decorator_passing_arbitrary_arguments
def function_with_no_argument():
    print("No arguments here")

function_with_no_argument()
# output:
# Do I have args?:
# ()
# {}
# No arguments here

@decorator_passing_arbitrary_arguments
def function_with_arguments(a, b, c):
    print(a, b, c)

function_with_arguments(1, 2, 3)
# output:
# Do I have args?:
# (1, 2, 3)
# {}
# 1 2 3

@decorator_passing_arbitrary_arguments
def function_with_named_arguments(name, occupation="programmer"):
    print("%s is a %s" % (name, occupation))

function_with_named_arguments("Ngoc Anh", occupation="developer")
# output:
# Do I have args?:
# ('Ngoc Anh',)
# {'occupation': 'developer'}
# Ngoc Anh is a developer

class Husband(object):

    def __init__(self):
        self.age = 25

    @decorator_passing_arbitrary_arguments
    def say_your_age(self, lie=-3):
        print("I am %s, what did you think ?" % (self.age + lie))

na = Husband()
na.say_your_age()
# output:
# Do I have args?:
# (<__main__.Husband object at 0x7f1244b65278>,)
# {}
# I am 22, what did you think ?

Truyền tham số cho chính decorator

Bây giờ, nếu bạn muốn truyền tham số cho chính decorator thì phải làm thế nào? Bởi vì decorator nhận tham số là một hàm nên bạn không thể truyền tham số cho nó được.

Trước khi đến với giải pháp, chúng ta xem qua ví dụ sau:

# Decorator cũng là hàm THÔNG THƯỜNG
def my_decorator(func):
    print("This is an ordinary function")

    def wrapper():
        print("This is function return by decorator")
        func()
    return wrapper

# Do đó, bạn có thể gọi chúng là không cần dùng cú pháp `@`
def lazy_function():
    print("lllllaaaaazzzzzyyyyy")

decorated_function = my_decorator(lazy_function)
# outputs: This is an ordinary function

# Output của đoạn code trên là "This is an ordinary function" bởi
# vì đơn giản là bạn vừa gọi hàm nên nó có kết quả như vậy.  Không
# có gì khó hiểu ở đây cả.

@my_decorator
def lazy_function():
    print("lllllaaaaazzzzzyyyyy")

# outputs: This is an ordinary function

Hai cách làm cho kết quả giống hệt nhau. Đó là bởi vì khi bạn sử dụng cú pháp @ thì Python sẽ gọi và thực thi hàm có tên là tên của decorator như thực thi một hàm thông thường.

Điều này rất quan trọng, bởi vì bạn có thể dùng tên của decorator để trỏ tới chính hàm decorator đó hay không.

def decorator_maker():

    print("Make decorator and execute only once")

    def my_decorator(func):
        print("I am a decorator!")

        def wrapped():
            print ("I am the wrapper around the decorated function.")
            func()

        print("As the decorator, I return the wrapped function.")

        return wrapped

    print("As a decorator maker, I return a decorator")
    return my_decorator

# Bây giờ, chúng ta sẽ tạo ra một decorator, đơn giản chỉ là gọi
# các hàm
new_decorator = decorator_maker()
# output:
# Make decorator and execute only once
# As a decorator maker, I return a decorator

# Sau đó là decorate hàm mà chúng ta muốn
def decorated_function():
    print("I am the decorated function.")

decorated_function = new_decorator(decorated_function)
# output:
# I am a decorator!
# As the decorator, I return the wrapped function.

# Thử gọi hàm được decorate xem sao
decorated_function()
# output:
# I am the wrapper around the decorated function.
# I am the decorated function.

Chúng ta có thể làm điều tương tự bằng cách bỏ qua các biến trung gian

def decorated_function():
    print("I am the decorated function.")
decorated_function = decorator_maker()(decorated_function)
# output:
# Make decorator and execute only once
# As a decorator maker, I return a decorator
# I am a decorator!
# As the decorator, I return the wrapped function.

# Gọi hàm được decorate
decorated_function()
# output:
# I am the wrapper around the decorated function.
# I am the decorated function.

Thậm chí chúng ta có thể dùng cú pháp ngắn gọn hơn:

@decorator_maker()
def decorated_function():
    print("I am the decorated function.")
# output:
# Make decorator and execute only once
# As a decorator maker, I return a decorator
# I am a decorator!
# As the decorator, I return the wrapped function

# Thử gọi hàm xem sao
decorated_function()
# output:
# I am the wrapper around the decorated function.
# I am the decorated function.

Rất đơn giản, chúng ta có thể gọi và thực thi hàm trong chính cú pháp @.

Quay trở lại với bài toán của chúng ta, nếu chúng ta xây dựng một hàm trả kết quả về là một decorator thì chúng ta có thể truyền tham số cho hàm đó.

def decorator_maker_with_arguments(decorator_arg1, decorator_arg2):

    print("I make decorators! And I accept arguments:",
          decorator_arg1, decorator_arg2)

    def my_decorator(func):
        print("I am the decorator.  You passed me arguments:",
              decorator_arg1, decorator_arg2)

        # Don't confuse decorator arguments and function arguments!
        def wrapped(function_arg1, function_arg2):
            print("I am the wrapper around the decorated function.\n"
                  "I can access all the variables\n"
                  "\t- from the decorator: {0} {1}\n"
                  "\t- from the function call: {2} {3}\n"
                  "Then I can pass them to the decorated function"
                  .format(decorator_arg1, decorator_arg2,
                          function_arg1, function_arg2))
            return func(function_arg1, function_arg2)

        return wrapped

    return my_decorator


@decorator_maker_with_arguments("foo", "bar")
def decorated_function_with_arguments(function_arg1, function_arg2):
    print("I am the decorated function with arguments",
          function_arg1, function_arg2)

decorated_function_with_arguments("wife", "husband")
# output:
# I make decorators! And I accept arguments: foo bar
# I am the decorator.  You passed me arguments: foo bar
# I am the wrapper around the decorated function.
# I can access all the variables
#   - from the decorator: foo bar
#   - from the function call: wife husband
# Then I can pass them to the decorated function
# I am the decorated function with arguments wife husband

Việc truyền tham số vào các hàm lồng nhau như vậy có sự giúp đỡ của closure. Bạn có thể tìm hiểu thêm về closure ở bài viết này

Vậy là chúng ta đã có một hàm, và chúng ta có thể truyền tham số cho hàm đó và sử dụng nó làm decorator. Thậm chí chúng ta có thể dùng biến để truyền tham số.

var1 = "foo"
var2 = "bar"

@decorator_maker_with_arguments(var1, var2)
def decorated_function_with_arguments(function_arg1, function_arg2):
    print ("I am the decorated function with arguments:"
           function_arg1, function_arg2)

var3 = "wife"
var4 = "husband"
decorated_function_with_arguments(var3, var4)
# output:
# I make decorators! And I accept arguments: foo bar
# I am the decorator.  You passed me arguments: foo bar
# I am the wrapper around the decorated function.
# I can access all the variables
#   - from the decorator: foo bar
#   - from the function call: wife husband
# Then I can pass them to the decorated function
# I am the decorated function with arguments wife husband

Với cách làm như trên, bạn có thể xây dựng decorator nhận tham số như những hàm thông thường khác. Bạn cũng có thể sử dụng *args**kwargs nếu muốn. Tuy nhiên, cần lưu ý rằng, decorator chỉ được gọi duy nhất một lần. Khi một hàm đã được decorate rồi, bạn không thể thay đổi nó nữa. Bạn cũng không thể truyền tham số động được.

Decorate một decorator

Trong phần trước, tôi đã trình bày cách xây dựng một decorator nhận tham số đầu vào. Biện pháp là sử dụng thêm một hàm lồng nhau nữa. Tức là chúng ta đã bao bọc decorator bằng một hàm khác. Nghe quen thế nhỉ? Chính xác, đây chính là cách xây dựng một decorator.

Đây là một trick khá thú vị, decorate một decorator. Bởi vì decorator cũng là hàm nên nó hoàn toàn có thể được decorate bởi những decorator khác.

def decorator_with_args(decorator_to_enhance):
    """
    Hàm này sẽ được dùng làm decorator.  Nó sẽ decorate một hàm mà
    hàm đó cũng sẽ trở thành decorator.  Decorator này cho phép
    bạn xây dựng decorator có thể nhận bất kỳ tham số nào, mà
    không cần nhớ cách thực hiện chúng.
    """

    # We use the same trick we did to pass arguments
    def decorator_maker(*args, **kwargs):

        # Chúng ta tạo ra một decorator nhận đầu vào là một hàm,
        # nhưng có thể truyền tham số từ maker.
        def decorator_wrapper(func):

            # Chúng ta return kết quả của decorator gốc, thực ra
            # nó cũng là hàm THÔNG THƯỜNG (nhưng return một hàm
            # khác).  Lưu ý rằng, decorator phải sử dụng đúng
            # nguyên mẫu mới có thể chạy được.
            return decorator_to_enhance(func, *args, **kwargs)

        return decorator_wrapper

    return decorator_maker

Chúng ta có thể sử dụng nó như sau:

# Chúng ta sẽ tạo ra một hàm dùng làm decorator.  Và dùng
# decorator đã tạo ra ở trên cho hàm này.  Nguyên mẫu hàm phải là
# decorator(func, *args, **kwargs) thì mới hoạt động được.
@decorator_with_args
def decorated_decorator(func, *args, **kwargs):
    def wrapper(function_arg1, function_arg2):
        print("Decorated with", args, kwargs)
        return func(function_arg1, function_arg2)
    return wrapper

# Sau đó chúng ta có thể dùng decorator này với các hàm chúng a
# muốn và có thể truyền tham số tùy ý.

@decorated_decorator(42, 404, 1024)
def decorated_function(function_arg1, function_arg2):
    print("Hello", function_arg1, function_arg2)

decorated_function("Universe and", "everything")
# output:
# Decorated with (42, 404, 1024) {}
# Hello Universe and everything

Lưu ý

  • Decorator được giới thiệu từ phiên bản Python 2.4 nên bạn cần sử dụng Python đúng phiên bản mới có thể dùng được.
  • Mặc dù cú pháp đơn giản, nhưng decorator không làm hàm nhanh hơn, thậm chí nó còn làm hàm chậm đi.
  • Bạn không thể bỏ decorator cho một hàm. Một khi hàm đã được decorate rồi, thì nó sẽ được decorate mãi mãi.
  • Vì decorator bao bọc các hàm nên rất khó để debug. Tuy nhiên, đã có giải pháp nếu bạn sử dụng Python 2.5 trở nên.

Từ phiên bản 2.5 trở nên, Python có module functools trong đó có hàm functools.wraps giúp chúng ta copy tên, module, docstring, v.v. của hàm được decorate vào hàm wrapper của chúng ta.

# Để debug, chúng a in ra tên của hàm
def foo():
    print("foo")

print(foo.__name__) # output: foo

# Nếu dùng decorator, mọi thứ khác đi nhiều.
def bar(func):
    def wrapper():
        print("bar")
        return func()
    return wrapper

@bar
def foo():
    print("foo")

print(foo.__name__) # output: wrapper

# "functools" có thể giúp chúng ta

import functools

def bar(func):
    # Đây là wrapper bao bọc hàm được decorator.  Tuy nhiên, sự kỳ
    # diệu ở đây là nhờ functools.
    @functools.wraps(func)
    def wrapper():
        print("bar")
        return func()
    return wrapper

@bar
def foo():
    print("foo")

print(foo.__name__) # output: foo

Decorator có thực sự cần thiết hay không?

Tất nhiên là cần thiết thì người ta mới có cú pháp cho decorator. Tuy nhiên, câu hỏi đặt ra là khi nào thì sử dụng decorator.

Decorator có thể được sử dụng để debug mà không cần thay đổi hàm hay thư viện. Ví dụ chúng ta có thể dùng decorator để debug và đánh giá hiệu năng các tính năng mà không cần thay đổi code của chúng.

def benchmark(func):
    """
    Decorator in ra thời gian thực thi hàm
    """
    import time

    def wrapper(*args, **kwargs):
        t = time.clock()
        res = func(*args, **kwargs)
        print(func.__name__, time.clock() - t)
        return res
    return wrapper


def logging(func):
    """
    Decorator ghi log về hoạt động của code. Ở đây chỉ đơn giản
    là in ra tên hàm nào đang được gọi.
    """
    def wrapper(*args, **kwargs):
        res = func(*args, **kwargs)
        print(func.__name__, args, kwargs)
        return res
    return wrapper


def counter(func):
    """
    Decorator đếm và in ra số lần hàm được gọi trong chương
    trình.
    """
    def wrapper(*args, **kwargs):
        wrapper.count = wrapper.count + 1
        res = func(*args, **kwargs)
        print("{0} has been used: {1}x".format(
            func.__name__, wrapper.count))
        return res
    wrapper.count = 0
    return wrapper


@counter
@benchmark
@logging
def reverse_string(string):
    return string[::-1]

print(reverse_string("Able was I ere I saw Elba"))
print(reverse_string("Lorem ipsum dolor sit amet, et cetero complectitur "
                     "mel, sea mundi delectus gubergren et.  In pro postea "
                     "ullamcorper, per dissentias vituperatoribus et.  Sed "
                     "saepe quaeque voluptaria et, et quod dolores "
                     "voluptaria vel.  An cum causae mediocritatem "
                     "vituperatoribus.  Est an nobis adversarium.  Probo "
                     "scripta omnesque ne sed, delicata volutpat no per."))

# output:
# reverse_string ('Able was I ere I saw Elba',) {}
# wrapper 2.6999999999999247e-05
# wrapper has been used: 1x
# ablE was I ere I saw elbA
# reverse_string ('Lorem ipsum dolor sit amet, et cetero complectitur mel, sea mundi delectus gubergren et.  In pro postea ullamcorper, per dissentias vituperatoribus et.  Sed saepe quaeque voluptaria et, et quod dolores voluptaria vel.  An cum causae mediocritatem vituperatoribus.  Est an nobis adversarium.  Probo scripta omnesque ne sed, delicata volutpat no per.',) {}
# wrapper 1.2999999999999123e-05
# wrapper has been used: 2x
# .rep on taptulov ataciled ,des en euqsenmo atpircs oborP .muirasrevda sibon na tsE .subirotareputiv metatircoidem easuac muc nA .lev airatpulov serolod douq te ,te airatpulov euqeauq epeas deS .te subirotareputiv saitnessid rep ,reprocmallu aetsop orp nI .te nergrebug sutceled idnum aes ,lem rutitcelpmoc oretec te ,tema tis rolod muspi meroL

Điều tuyệt vời của decorator là bạn có thể dùng nó với bất cứ thứ gì mà không cần thay đổi nội dung.

@counter
@benchmark
@logging
def get_an_excuse():
    import urllib.request
    import re
    handle = urllib.request.urlopen("http://programmingexcuses.com/")
    response = handle.read().decode()
    return re.search("\n.+>([,'.?!\w\s]+)</a>", response).group(1)

print(get_an_excuse())
print(get_an_excuse())
# output:
# get_an_excuse () {}
# wrapper 0.07300399999999999
# wrapper has been used: 1x
# Our internet connection must not be working
# get_an_excuse () {}
# wrapper 0.0027760000000000007
# wrapper has been used: 2x
# That process requires human oversight that nobody was providing

Python bản thân nó cũng có sẵn rất nhiều decorator như @property, @classmethod, v.v… Django cũng thường xuyên sử dụng decorator, nhất là khi phân quyền cho người dùng.

Decorator là một công cụ rất mạnh mẽ, nên nếu sử dụng đúng, nó sẽ cho chúng ta những tiện ích tuyệt vời.

Bonus: Decorator mà vẫn giữ được hàm gốc

Như phần trước tôi đã nói, một hàm khi đã được decorate rồi thì chúng ta không thể bỏ decorator đi được. Bất cứ khi nào bạn gọi hàm, thực ra bạn đang gọi hàm đã được decorate.

Nhưng nhiều trường hợp, bạn chỉ muốn thực thi hàm gốc thôi. Vậy phải làm thế nào? Không lẽ phải định nghĩa lại hàm.

Thực ra có một trick để làm việc này. Nói chung nó rất dễ hiểu nên chắc chỉ cần nhìn code là hiểu ngay.

def decorator_keep_original_function(function_to_decorate):

    def wrapper(*args, **kwargs):
        print("This is decorated function")
        function_to_decorate(*args, **kwargs)

    # Chúng ta sẽ lưu hàm gốc chưa được decorate như là một
    # thuộc tính của hàm mới.
    wrapper._original_function = function_to_decorate
    return wrapper


@decorator_keep_original_function
def some_function():
    print("This is original function")

# Gọi hàm đã được decorate
some_function()
# output:
# This is decorated function
# This is original function

# Chúng ta vẫn có thể gọi hàm gốc chứa decorate
some_function._original_function()
# output:
# This is original function

Có một cách khác, đó là sử dụng __closure__ để lấy lại nội dung của hàm gốc. Cách này chỉ hoạt động nếu chúng ta có sử dụng closure. Lưu ý rằng, không phải tất cả các hàm lồng nhau đều là closure.

def undecorator(decorated_function):
    # Python < 2.6 sử dụng `func_closure` thay thế cho
    # `__closure__`
    return decorated_function.__closure__[0].cell_contents


def decorator_to_undo(function_to_decorate):

    def wrapper(*args, **kwargs):
        print("This is decorated function")
        function_to_decorate(*args, **kwargs)

    return wrapper


@decorator_to_undo
def some_function():
    print("This is original function")

# Hàm đã được decorate
some_function()
# output:
# This is decorated function
# This is original function

# Thử undecorate xem sao
undecorator(some_function)()
# output:
# This is original function
# => Đây chính là điều chúng ta mong đợi.

Kết luận

Trên đây là những hiểu biết của tôi về decorator được dùng trong Python. Decorator là một công cụ rất hữu dụng và được sử dụng khá thường xuyên. Hy vọng bài viết cho bạn phần nào hiểu biết về decorator, cách viết và sử dụng chúng.

Tôi xin lỗi nếu bài viết có bất kỳ typo nào. Nếu bạn nhận thấy điều gì bất thường, xin hãy cho tôi biết.

Nếu có bất điều gì muốn nói, bạn có thể liên hệ với tôi qua các mạng xã hội, tạo discussion hoặc report issue trên Github.

Welcome

manhhomienbienthuy

Đây là thế giới của manhhomienbienthuy (naa). Chào mừng đến với thế giới của tôi!

Bài viết liên quan

Bài viết mới

Chuyên mục

Lưu trữ theo năm

Thông tin liên hệ

Cảm ơn bạn đã quan tâm blog của tôi. Nếu có bất điều gì muốn nói, bạn có thể liên hệ với tôi qua các mạng xã hội, tạo discussion hoặc report issue trên Github.