Tag: advanced

  • Python Deep Dive: Hiểu closures, decorators và các ứng dụng của chúng – Phần 4

    Python Deep Dive: Hiểu closures, decorators và các ứng dụng của chúng – Phần 4

    Python Deep Dive: Hiểu closures, decorators và các ứng dụng của chúng – Phần 4

    Ở bài viết này, tôi sẽ giới thiệu một kỹ thuật gọi là decorator. Nhìn chung, nếu ai đã từng làm việc với các python web framework như Django, Flask, FastAPI,… thì sẽ sử dụng kỹ thuật này thường xuyên, nhưng không phải ai cũng hiểu rõ bản chất của nó.

    Thừa nhận rằng, khi sử dụng các decorators có sẵn mà các framework cung cấp, đã đủ để chúng ta làm việc như một web developer. Tuy nhiên, việc nắm được bản chất của kỹ thuật này cũng giúp cho chúng ta sử dụng decorators hiệu quả hơn, có thể tùy chỉnh và tạo ra các decorators của riêng mình, như vậy là ta đã trở thành một lập trình viên chuyên nghiệp hơn.

    Table of contents

    Decorators

    Nhắc lại một ví dụ trong Phần 3, chúng ta đã áp dụng Closure để maintain một bộ đếm – đếm số lần gọi một hàm bất kỳ.

    def counter(fn):
        cnt = 0  # số lần chạy fn, khởi tạo là 0
    
        def inner(*args, **kwargs):
            nonlocal cnt
            cnt = cnt + 1
            print('Hàm {0} đã được gọi {1} lần'.format(fn.__name__, cnt))
            return fn(*args, **kwargs)
    
        return inner

    Hàm counter nhận một function làm đầu vào – fn. Trong hàm counter, ta khởi tạo 1 biến cục bộ cnt – biến này sẽ đến số lần gọi hàm fn, mỗi khi goị đến hàm inner thì biến cnt được tăng lên 1 đơn vị.

    Việc sử dụng *args**kwargs trong hàm inner giúp ta có thể gọi hàm fn với bất kỳ sự kết hợp positional argumentskeyword-only arguments nào. Lấy ví dụ:

    def mult(x, y=10):
        """
        return the products of two values
        """
        return x * y
    
    mult = counter(mult) # trả về inner function - closure

    Nhớ rằng, ban đầu mult là một label trỏ đến hàm mult được định nghĩa ở trên. Goị hàm counter(mult) sẽ trả về một closure và gán vào một label là mult, thì lúc này mult sẽ trỏ đến closure đó, rõ ràng là khác so với nơi mà mult ban đầu trỏ đến.

    Tuy nhiên, inner thực hiện gọi hàm mult ban đầu cho chúng ta và trả về kết quả của nó, chính vì vậy mà kết quả trả về khi gọi hàm inner không khác so với việc gọi hàm mult ban đầu. Nhưng thực sự, hàm inner đã làm thêm một số việc trước khi gọi và trả về kết quả của hàm mult, đó là đếm số lần hàm mult được gọi.

    mult(3, 5)
    # In ra: Hàm mult đã được gọi 1 lần
    # return 15

    Chúng ta đã thực sự sửa đổi hàm mult ban đầu, bằng cách wrapping nó bên trong một hàm khác – bổ sung thêm chức năng cho nó –> chúng ta đã decorated hàm mult với hàm counter và chúng ta gọi counterdecorator function.

    Nhìn chung, một decorator function sẽ:

    • lấy 1 function như một đối số
    • return một closure
    • closure nói trên sẽ nhận bất kỳ sự kết hợp đầu vào nào (sử dụng *args và **kwargs)
    • chạy một vài chức năng bên trong closure
    • closure function sẽ gọi original function sử dụng các đối số được truyền vào closure
    • return kết quả được trả về từ function call nói trên

    The @ Symbol

    Như đã thấy, chúng ta hoàn toàn có thể sử dụng decorator function như sau:

    def my_func(*args, **kwargs):
        # some code here
        # ...
    my_func = func(my_func)

    Trong đó, funcdecorator function, còn my_func là hàm được decorated.

    Tuy nhiên, có một cách khác thanh lịch hơn:

    @func
    def my_func(*args, **kwargs):
        # some code here
        # ...

    Introspection

    Sử dụng lại counter như một decorator cho hàm mult:

    import inspect
    
    @counter
    def mult(x, y=10):
        """
        return the products of two values
        """
        return x * y
    
    print(f'name = {mult.__name__}') # name = inner

    Ta thấy rằng, câu lệnh print ở trên sẽ in ra màn hình name = inner. Rõ ràng, tên của hàm mult không phải là mult nữa, mà nó là inner – tên của một closure, điều này chứng tỏ một điều rằng, hàm mult lúc này chính là một closure.

    Vì vậy, cầu lưu ý rằng, khi chúng ta decorate một function, tức là chúng ta đã làm cho function đó thay đổi (về bản chất).

    Để chắc chắn hơn, hãy thử gọi:

    print(f'signature: {inspect.signature(mult)}') # signature: (*args, **kwargs)

    Như vậy, khi gọi inspect.signature(mult), chúng ta đã nhìn thấy rõ chữ ký của hàm inner được trả về.

    Việc decorate một function làm thay đổi docstring, signature, name khiến cho việc debugging trở nên khó khăn hơn rất nhiều.

    The wraps function

    Để giải quyết vấn đề mới nói ở trên, functools module cung cấp một wraps function có thể fix metadata của inner function trong decorator. Bản thân wraps function này là một decorator.

    Hàm wraps này sẽ decorate inner function để thay đổi metadata (docstring, signature, name,…) của nó. Thêm nữa, nó phải sử dụng hàm mult như một đối số đầu vào, để có thể thay thế metadata của inner function bằng metadata của mult function (hàm được decorated)

    from functools import wraps
    import inspect
    
    def counter(fn):
        cnt = 0  # số lần chạy fn, khởi tạo là 0
    
        @wraps(fn)
        def inner(*args, **kwargs):
            nonlocal cnt
            cnt = cnt + 1
            print('Hàm {0} đã được gọi {1} lần'.format(fn.__name__, cnt))
            return fn(*args, **kwargs)
        return inner
    
    @counter
    def mult(x, y=10):
        """
        return the products of two values
        """
        return x * y
    
    print(f'name = {mult.__name__}') # name = mult
    print(f'signature: {inspect.signature(mult)}') # signature: (x, y=10)

    Chúng ta không nhất thiết phải sử dụng wraps function trong khi sử dụng decorator, nhưng việc sử dụng nó sẽ giúp cho việc debugging trở nên dễ dàng hơn.

    Summary

    1. Decorators trong Python là cú pháp cho phép một function sửa đổi một function khác tại thời gian chạy (runtime)
    2. Ký pháp @ giúp cho việc sử dụng decorator thanh lịch hơn (mặc dù có thể không cần)
    3. Hãy sử dụng wraps function giúp cho việc debugging dễ dàng hơn khi làm việc với decorators.

    References

    [1] Fred Baptiste, Python Deep Dive, Part 1

    [2] Brett Slatkin, Effective Python, Item 26

    Authors

    [email protected]

  • Python Deep Dive: Hiểu closures, decorators và các ứng dụng của chúng – Phần 3

    Python Deep Dive: Hiểu closures, decorators và các ứng dụng của chúng – Phần 3

    Trong lập trình với Python thì Functional Programming đóng một vai trò vô cùng quan trọng và các functions trong Python là các first-class citizens. Điều đó có nghĩa là chúng ta có thể vận hành các functions giống như các objects khác:

    • Truyền các function giống như các đối số.
    • Gán một function cho một biến số.
    • Return một function từ một function khác.

    Dựa trên những điều này, Python hỗ trợ một kỹ thuật vô cùng mạnh mẽ: closures. Sau khi hiểu closures, chúng ta sẽ đi đến tiếp cận một khái niệm rất quan trọng khác – decorators. Đây là 2 khái niệm/kỹ thuật mà bất kỳ lập trình viên Python chuyên nghiệp nào cũng cần phải nắm vững.

    Trong phần 3 này, tôi sẽ giới thiệu một số ví dụ ứng dụng closure để viết code hiệu quả hơn.

    Bài viết này yêu cầu kiến thức tiên quyết về scopes, namespaces, closures trong Python. Nếu bạn chưa tự tin, thì nên đọc trước 2 bài viết dưới đây (theo thứ tự):

    Table of contents

    Closure

    Nhắc lại

    Closure có thể tránh việc lợi dụng các giá trị global và cung cấp một cách thức ẩn dữ liệu (data hiding), cung cấp một giải pháp object-oriented cho vấn đề. Khi chỉ có một vài phương thức được triển khai trong một class, thì closure có thể cung cấp một giải pháp thay thế nhưng thanh lịch hơn. Khi số lượng thuộc tính và phương thức tăng lên nhiều, thì sử dụng class sẽ phù hợp hơn. Các tiêu chí sau cho thấy closure trong Python khi một nested function có tham chiếu một giá trị trong enclosing scope:

    • Tồn tại một nested function (function bên trong function khác)
    • Nested function có tham chiếu đến một giá trị được khai báo bên trong enclosing function.
    • Enclosing function trả về nested function (giá trị được return)

    Nguồn ảnh: Andre Ye

    Averager

    Trong ví dụ này, ta sẽ xây dựng một hàm tính giá trị trung bình của nhiều giá trị sử dụng closure. Hàm này có thể tính giá trị trung bình theo thời gian bằng cách thêm các đối số vào hàm đó mà không cần phải lặp lại việc tính tổng các giá trị trước đó.

    Cách tiếp cận dễ dàng nghĩ đến nhất là sử dụng class trong Python, ở đó ta sẽ sử dụng một biến instance để lưu trữ tổng của dãy số và số số hạng. Sau đó cung cấp cho class đó một method để thêm vào 1 số hạng mới, và trả về giá trị trung bình cộng của dãy số.

    class Averager:
        def __init__(self):
            self._count = 0
            self._total = 0
    
        def add(self, value):
            self._total += value
            self._count += 1
            return self._total / self._count
    
    a = Averager()
    a.add(1) # return 1.0
    a.add(2) # return 1.5
    a.add(3) # return 2.0

    Bằng cách sử dụng closure, ta có thể tận dụng functions trong python để xây dựng được tính năng tương tự việc sử dụng class, nhưng thanh lịch và hiệu quả hơn.

    def averager():
        total = 0
        count = 0
    
        def add(value):
            nonlocal total, count
            total += value
            count += 1
            return 0 if count == 0 else total / count
    
        return add
    
    a = averager()
    a(1) # return 1.0
    a(2) # return 1.5
    a(3) # return 2.0

    Counter

    Áp dụng closure, ta có thể xây dựng 1 bộ đếm, đếm số lần gọi một function mỗi khi function đó chạy. Function này có thể nhận bất kỳ đối số hoặc đối số từ khóa nào.

    def counter(fn):
        cnt = 0  # số lần chạy fn, khởi tạo là 0
    
        def inner(*args, **kwargs):
            nonlocal cnt
            cnt = cnt + 1
            print('{0} has been called {1} times'.format(fn.__name__, cnt))
            return fn(*args, **kwargs)
    
        return inner

    Giả sử ta muốn bổ sung thêm việc đếm số lần gọi hàm tính tổng 2 số:

    def add(a, b):
        return a + b

    Ta có thể áp dụng closure như sau:

    count_sum = counter(add))
    count_sum(1, 2) # sum has been called 1 times
    count_sum(3, 5) # sum has been called 2 times

    Sở dĩ hàm count_sum có thể làm được như trên là bởi vì nó đang sở hữu 2 free variables là:

    • fn: tham chiếu đến hàm add
    • cnt: duy trì đếm số lần gọi hàm fn
    count_sum.__code__.co_freevars # ('cnt', 'fn')

    Đến đây, thay vì in ra standard output số lần gọi 1 hàm bất kỳ (hàm add chỉ là 1 ví dụ), ta có thể sử dụng 1 từ điển là global variable lưu trữ các cặp {key: value}. Ở đó, key là tên của hàm và value là số lần gọi hàm. Để làm được điều đó, ta cần sửa đổi một chút ở hàm counter bằng cách bổ sung thêm cho nó 1 đối số là tham chiếu đến từ điển lưu trữ:

    def counter(fn, counters):
        cnt = 0  # số lần chạy fn, khởi tạo là 0
    
        def inner(*args, **kwargs):
            nonlocal cnt
            cnt = cnt + 1
            counters[fn.__name__] = cnt  # counters là nonlocal
            return fn(*args, **kwargs)
    
        return inner
    func_counters = dict() # khởi tạo từ điển
    # đếm số lần chạy hàm add
    counted_add = counter(add, func_counters)
    for i in range(10):
        counted_add(i, i+1)

    Biến func_counters là biến toàn cục, vì vậy ta có thể bổ sung thêm từ khóa là tên của hàm khác vào nó, thử 1 ví dụ, xét hàm nhân 2 số:

    def mult(a, b):
        return a * b
    
    counted_mult = counter(mult, func_counters)
    for i in range(7):
        counted_mult(i, i)

    Biến func_counters lúc này sẽ cho chúng ta biết số lần gọi hàm add và số lần gọi hàm mult

    func_counters ## {'mult': 7, 'add': 10}

    Cả 2 hàm counted_addcounted_mult đều đang giữ 3 free variables:

    • fn: tham chiếu đến hàm cần đếm
    • cnt: duy trì đếm số lần gọi hàm fn
    • counters: tham chiếu đến từ điển lưu trữ thông tin về số lần đếm các hàm

    Hãy thử nghĩ, nếu như, thay vì ta gọi:

    counted_add = counter(add, func_counters)

    Ta gọi như sau:

    add = counter(add, func_counters)

    Lúc này, ta có một hàm add mới, thực sự không khác hàm add lúc đầu về tính năng là tính tổng 2 số. Tuy nhiên sau khi gọi hàm add, lúc này ta còn nhận được thêm thông tin về số lần gọi hàm add được giữ trong biến func_counters.

    Như vậy, hàm counter đóng vai trò như 1 trình trang trí cho hàm add (tương tự với hàm mult), nó bổ sung thêm tính năng cho hàm add nhưng không thay đổi hành vi của hàm ađd (trả về tổng 2 số). Đây là tính chất quan trọng của decorator mà chúng ta sẽ tìm hiểu trong một bài viết sau.

    Use Closures Skilfully

    Closure là một vũ khí mạnh mẽ của Python. Người mới bắt đầu có thể gặp đôi chút khó khăn trong việc áp dụng nó trong việc viết mã. Tuy nhiên, nếu ta có thể hiểu và sử dụng nó một cách thuần thục, thì nó sẽ vô cùng hữu ích.

    Trên thực tế thì decorator trong Python là một sự mở rộng của closure. Chúng ta sẽ bàn về decorator sau, nhưng ai cũng biết rằng hầu hết các framework sử dụng Python cho web development đều sử dụng decorator rất thường xuyên.

    Dưới đây là 2 tips quan trọng sẽ giúp bạn sử dụng closure thuần thục:

    Sử dụng lambda function để đơn giản hóa code

    Xét 1 ví dụ:

    def outer_func():
        name = "Tu Anh"
    
        def print_name():
            print(name)
    
        return print_name
    
    f = outer_func()
    print(outer_func.__closure__) # None
    print(f.__closure__) # (<cell at 0x7f31445b2e90: str object at 0x7f314459c070>,)
    print(f.__closure__[0].cell_contents) # Tu Anh

    Ta có thể làm cho ví dụ trên thanh lịch hơn bằng cách sử dụng lambda function:

    def outer_func():
        name = "Tu Anh"
    
        return lambda _: print(name)
    
    f = outer_func()
    print(outer_func.__closure__) # None
    print(f.__closure__) # (<cell at 0x7f31445a44d0: str object at 0x7f31445b6070>,)
    print(f.__closure__[0].cell_contents) # Tu Anh

    Closures che giấu các biến private hiệu quả hơn

    Trong Python thì không có các từ khóa built-in như là public hay private để kiểm soát khả năng truy cập của các biến. Theo quy ước, chúng ta sử dụng double underscores để định nghĩa một member của 1 class là private. Tuy nhiên, chúng vẫn có thể được truy cập.

    Đôi khi, chúng ta cần bảo vệ mạnh mẽ hơn để ẩn một biến. Và closures có thể giải quyết vấn đề này. Như ví dụ ở trên, thì khó để ta có thể truy cập và thay đổi được giá trị của biến name trong hàm f. Như vậy, biến name dường như đã private hơn.

    References

    [1] Andre Ye, Essential Python Concepts & Structures Any Serious Programmer Needs to Know, Explained

    [2] Fred Baptiste, Python Deep Dive, Part 1

    [3] Yang Zhou, 5 Levels of Understanding Closures in Python

    Authors

    [email protected]

  • Python Deep Dive: Hiểu closures, decorators và các ứng dụng của chúng – Phần 2

    Python Deep Dive: Hiểu closures, decorators và các ứng dụng của chúng – Phần 2

    Trong lập trình với Python thì Functional Programming đóng một vai trò vô cùng quan trọng và các functions trong Python là các first-class citizens. Điều đó có nghĩa là chúng ta có thể vận hành các functions giống như các objects khác:

    • Truyền các function giống như các đối số.
    • Gán một function cho một biến số.
    • Return một function từ một function khác.

    Dựa trên những điều này, Python hỗ trợ một kỹ thuật vô cùng mạnh mẽ: closures. Sau khi hiểu closures, chúng ta sẽ đi đến tiếp cận một khái niệm rất quan trọng khác – decorators. Đây là 2 khái niệm/kỹ thuật mà bất kỳ lập trình viên Python chuyên nghiệp nào cũng cần phải nắm vững.

    Bài viết này yêu cầu kiến thức tiên quyết về scopes, namespace trong Python. Nếu bạn chưa tự tin, vui lòng đọc trước Phần 1

    Table of contents

    Free Variables and Closures

    Nhắc lại rằng: Các functions được xác định bên trong function khác có thể truy cập các biến bên ngoài (nonlocal)

    def outer():
        x = 'python'
        def inner():
            # x trỏ đến cùng một object mà biến x bên ngoài trỏ tới.
            print("{0} rocks!".format(x))
        inner()
    outer() # python rocks! --> Đây được gọi là một closure.

    Biến nonlocal x trong hàm inner được gọi là free variable. Khi chúng ta xem xét hàm inner, chúng ta thực sự đang thấy:

    • hàm inner
    • free variable x (đang có giá trị là ‘python’)

    Xin lưu ý rằng biến x trong hàm inner không thuộc local scope của hàm đó, mà nó nằm ở một nơi khác. Nhãn x này và nhãn x thuộc hàm outer liên kết lại với nhau, được gọi là closure.

    Returning the inner function

    Vậy điều gì sẽ xảy ra nếu như chúng ta không gọi hàm inner() bên trong hàm outer() mà thay vào đó, ta return nó. Khi gọi hàm outer(), hàm inner sẽ được tạo, và outer trả về hàm inner. Khi đó, closure nói trên vẫn đang còn tồn tại, chúng không bị mất đi. Vì vậy, khi gọi hàm outer(), trả về hàm inner, thực sự là chúng ta đang trả về một closure.
    Chúng ta có thể gán giá trị trả về từ hàm outer() cho một tên biến, ví dụ:

    fn = outer() # fn là closure
    fn() # python rocks!

    Khi chúng ta gọi hàm fn(), tại thời điểm đó – Python xác định giá trị của x trong một extended scope. Lưu ý rằng, hàm outer() đã chạy xong và đã kết thúc trước khi gọi hàm fn() –> scope của hàm outer đã được giải phóng. Vậy tại sao khi gọi hàm fn(), chúng ta vẫn nhận về được giá trị ‘python rocks!’ !!? –> closure.
    Thật magic! Để hiểu rõ hơn về closure, bạn hãy uống một chén trà rồi ngồi đọc tiếp nhé ;).

    Python Cells and Multi-Scoped Variables

    Xét ví dụ đơn giản sau:

    def outer():
        x = 'tuanh'
        def inner():
            print(x)
        return inner

    Giá trị của biến x được chia sẻ giữa 2 scope:

    • outer
    • closure

    Nhãn (label, name) x nằm trong 2 scope khác nhau nhưng luôn luôn refer tới cùng 1 giá trị. Python làm điều này bằng cách sử dụng một đối tượng trung gian, cell object.

    cell object đóng vai trò trung gian, và x sẽ tham chiếu gián tiếp đến đối tượng có giá trị ‘tuanh’. Trên thực tế, cả 2 biến x (trong outer và inner) đều trỏ đến cùng một cell object. Và khi chúng ta request giá trị của biến, Python thực hiện “double-hop” để lấy về giá trị cuối cùng.
    Bây giờ, chúng ta đã hiểu tại sao khi hàm outer() kết thúc, chúng ta vẫn có thể lấy được giá trị của biến x trong hàm inner rồi chứ.

    Closures

    Đến đây, chúng ta có thể nghĩ về closure như là một function + extended scope – scope mà chứa free variables.
    Giá trị của free variable là object mà cell trỏ tới. Mỗi khi function trong closure được gọi và free variable được tham chiếu:

    • Python tìm kiếm cell object, và sau đó bất kỳ cái gì cell đang trỏ tới.
    Nguồn: Fred Baptiste (Python Deep Dive – Functional)

    Introspection

    Chúng ta tiếp tục sử dụng ví dụ như trước:

    Nguồn: Fred Baptiste (Python Deep Dive – Functional)

    (more…)