+2

Primer on Python Decorators (Translated Article)

Trong tutorial mở đầu này, chúng ta sẽ tìm hiểu xem decorator là gì và làm thế nào để tạo và sử dụng chúng. Decorator cung cấp cú pháp (syntax) đơn giản để gọi function cấp cao hơn (higher-class function). Theo định nghĩa, decorator là một function nhận một function khác và mở rộng behaviour của function này mà không chỉnh sửa nó một cách tường minh. Nghe có vẻ khó hiểu - nhưng không thực sự vậy, đặc biệt là sau khi chúng ta đi qua một số ví dụ.

decorators.png

Functions

Trước khi bạn có thể hiểu decorator, đầu tiên bạn phải hiểu cách một function hoạt động. Về cơ bản, function sẽ trả về một giá trị dựa vào các tham số đầu vào.


def foo(bar):
    return bar + 1

print(foo(2) == 3)

First Class Objects

Trong Python, function là các first-class object. Điều này có nghĩa là function có thể được truyền và sử dụng như một tham số đầu vào, trả về từ một function cũng như là được assign cho một biến (variable).


def foo(bar):
    return bar + 1

print(foo)
print(foo(2))
print(type(foo))

def call_foo_with_arg(foo, arg):
    return foo(arg)

print(call_foo_with_arg(foo, 3))

Nested Functions

Bởi vì đặc tính first-class của function trong Python, bạn có thể định khai báo một function trong một function khác. Các function như vậy gọi là nested function.


def parent():
    print("Printing from the parent() function.")

    def first_child():
        return "Printing from the first_child() function."

    def second_child():
        return "Printing from the second_child() function."

    print(first_child())
    print(second_child())

Điều gì sẽ xảy ra nếu bạn gọi function parent()? Nghĩ về điều này một chút. Bạn sẽ nhận được:

Printing from the parent() function.
Printing from the first_child() function.
Printing from the second_child() function

Thử gọi hàm first_child(), bạn sẽ nhận được lỗi:

Traceback (most recent call last):
File "decorator03.py", line 15, in <module>
first_child()
NameError: name 'first_child' is not defined

What have we learned?

Bất cứ khi nào bạn gọi function parent(), các function con, first_child()second_child() cũng sẽ được gọi theo và bởi vì tính chất scope, cả hai function con này đều không tồn tại bên ngoài function cha, tức là chúng ta không thể được gọi trực tiếp từ bên ngoài function parent().

Returning Functions

Python cũng cho phép bạn trả về một function từ một function khác. Hãy sửa lại function trong ví dụ trước như sau:


def parent(num):

    def first_child():
        return "Printing from the first_child() function."

    def second_child():
        return "Printing from the second_child() function."

    try:
        assert num == 10
        return first_child
    except AssertionError:
        return second_child

foo = parent(10)
bar = parent(11)

print(foo)
print(bar)

print(foo())
print(bar())

Output của hai lệnh print đầu tiên là:

<function first_child at 0x1004a8c08>
<function second_child at 0x1004a8cf8>

Điều này đơn giản có nghĩa là foo trỏ đến function first_child() trong khi bar trỏ đến function sencond_child()

Output của hai lệnh print sau:

Printing from the first_child() function.
Printing from the second_child() function.

Sau cùng, bạn có để ý rằng trong ví dụ thứ ba, chúng ta thực thi các hàm con trong hàm cha - ví dụ như second_child(). Trong khi ở ví dụ cuối cùng, chúng ta không thêm cặp dấu ngoặc đơn () vào các hàm con - first_child - khi được gọi như vậy, chúng ta có thể sử dụng chúng trong tương lai. Okay?

Bây giờ thì chúng ta đã sẵn sàng tiếp nhận decorator.

Decorators

Hãy cùng xét hai ví dụ sau:

Example 1


def my_decorator(some_function):

    def wrapper():

        print("Something is happening before some_function() is called.")

        some_function()

        print("Something is happening after some_function() is called.")

    return wrapper

def just_some_function():
    print("Wheee!")

just_some_function = my_decorator(just_some_function)

just_some_function()

Các bạn có thể đoán được output không? Hãy thử xem 😃

Something is happening before some_function() is called.
Wheee!
Something is happening after some_function() is called.

Let’s take it one step further and add an if statement.

Để hiểu được điều gì đang diễn ra, hãy quay lại với bốn ví dụ trước đó. Đơn thuần chỉ là chúng ta đang áp dụng những gì chúng ta đã học. Một cách ngắn gọn, decorator wrap một function và sửa đổi behaviour của nó.

Hãy đi xa hơn một chút, chúng ta thêm một khối lệnh if...else...:

Example 2


def my_decorator(some_function):

    def wrapper():

        num = 10

        if num == 10:
            print("Yes!")
        else:
            print("No!")

        some_function()

        print("Something is happening after some_function() is called.")

    return wrapper

def just_some_function():
    print("Wheee!")

just_some_function = my_decorator(just_some_function)

just_some_function()

Output sẽ là:

Yes!
Wheee!
Something is happening after some_function() is called.

Syntactic sugar!

Python cho phép bạn đơn giản hóa việc gọi decorator sử dụng ký hiệu @.

Cùng tạo một module cho decorator của chúng ta nào:


NeverLand.py

def my_decorator(some_function):

    def wrapper():

        num = 10

        if num == 10:
            print("Yes!")
        else:
            print("No!")

        some_function()

        print("Something is happening after some_function() is called.")

    return wrapper

if __name__ == "__main__":
    my_decorator()

Okay, hãy xem làm thế nào chúng ta có thể gọi một function với decorator:


from NeverLand import my_decorator

@my_decorator
def just_some_function():
    print("Wheee!")

just_some_function()

Nếu chạy ví dụ này, chúng ta sẽ nhận được output tương tự với ví dụ trước:

Yes!
Wheee!
Something is happening after some_function() is called.

Như vậy, @my_decorator tương đương với việc assign just_some_function = my_decorator(just_some_function). Đây là cách mà chúng ta apply một decorator cho một function.

Real World Examples

Bên trên chúng ta đã tìm hiểu sơ lược về decorator. Bây giờ chúng ta cùng xét một số ví dụ thực tế.


import time

def timing_function(some_function):

    """
    Outputs the time a function takes
    to execute.
    """

    def wrapper():
        t1 = time.time()
        some_function()
        t2 = time.time()
        return "Time it took to run the function: " + str((t2 - t1)) + "\n"
    return wrapper

@timing_function
def my_function():
    num_list = []
    for num in (range(0, 10000)):
        num_list.append(num)
    print("\nSum of all the numbers: " + str((sum(num_list))))

print(my_function())

Hãy chạy ví dụ trên và chắc chắn bạn hiểu được những gì diễn ra..


from time import sleep

def sleep_decorator(function):

    """
    Limits how fast the function is
    called.
    """

    def wrapper(*args, **kwargs):
        sleep(2)
        return function(*args, **kwargs)
    return wrapper

@sleep_decorator
def print_number(num):
    return num

print(print_number(222))

for num in range(1, 6):
    print(print_number(num))

Decorator này được sử dụng để làm chậm việc xử lý function print_number().

Một trong những decorator được sử dụng nhiều nhất trong Python là login_required(), decorator này dùng để đảm bảo rằng một user đã log in hay được xác thực hợp lệ trước khi họ có thể truy cập vào một route nào đó (/secret trong trường hợp này):


from functools import wraps
from flask import g, request, redirect, url_for

def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if g.user is None:
            return redirect(url_for('login', next=request.url))
        return f(*args, **kwargs)
    return decorated_function

@app.route('/secret')
@login_required
def secret():
    pass

Bạn có để ý là function được truyền đến decorator functools.wraps() hay không? Điều này đơn giản là để giữ lại metadata của function được wrap.

Hãy cùng xét usecase cuối cùng. Nhìn nhanh vào đoạn xử lý route Flask sau:


@app.route('/grade', methods=['POST'])
def update_grade():
    json_data = request.get_json()
    if 'student_id' not in json_data:
        abort(400)
    # update database
    return "success!"

Ở đây chúng ta đảm bảo rằng key student_id là một phần của request. Mặc dù việc validate này không thực sự thuộc về bản thân function. Cộng với việc có lẽ còn có những route khác cũng dùng chung validate này. Như vậy, hãy giữ nó được DRY với một decorator.


from flask import Flask, request, abort
from functools import wraps

app = Flask(__name__)

def validate_json(*expected_args):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            json_object = request.get_json()
            for expected_arg in expected_args:
                if expected_arg not in json_object:
                    abort(400)
            return func(*args, **kwargs)
        return wrapper
    return decorator

@app.route('/grade', methods=['POST'])
@validate_json('student_id')
def update_grade():
    json_data = request.get_json()
    print(json_data)
    # update database
    return "success!"

Trong đoạn code trên, decorator chấp nhận tham số có độ dài thay đổi (variable-length argument) nên chúng ta có thể truyền không giới hạn số lượng string đầu vào, mỗi string là một key được sử dụng để validate dữ liệu JSON. Bạn có để ý việc này sẽ tạo ra các decorator một cách linh động dựa vào những string này. Hãy thử xem!

Bạn có thể tìm thấy source code trong bài viết này ở đây here

Cheers!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí