Python – Function Decorator & Wrap

測試環境為 CentOS 8 x86_64 (虛擬機)

關於 Syntactic sugar 請參考 – https://zh.wikipedia.org/zh-tw/%E8%AF%AD%E6%B3%95%E7%B3%96

語法糖(英語:Syntactic sugar)是由英國電腦科學家彼得·蘭丁發明的一個術語,指電腦語言中添加的某種語法,這種語法對語言的功能沒有影響,但是更方便程式設計師使用。語法糖讓程式更加簡潔,有更高的可讀性。

在 Python 的 syntax candy(syntactic sugar) 被稱作 Decorator (使用 @ 當做 syntax candy),這邊來看一下 Decorator(裝飾器) 與 wrapper 怎麼來使用.

參考範例 – https://dev.to/codemee/python-decorator-3bni

無使用 Decorator (裝飾器)

  • 範例 : 建立一個從 1 加到 10000 的函數.
    [root@localhost ~]# vi NoDecorator1.py
    def works():
        total = 0
        for i in range(10000):
            total += i
        print("total:", total)
    
    works()
    

    執行結果

    [root@localhost ~]# python3 NoDecorator1.py
    total: 49995000
    
  • 範例 : 建立一個從 1 加到 10000 的函數與其計算其執行時間.
    [root@localhost ~]# vi NoDecorator2.py
    import time
    def timing(func):
        print("Start...")
        t1 = time.perf_counter()
        func()
        t2 = time.perf_counter()
        print("Elapsed time(secs):", t2 - t1)
    
    def works():
        total = 0
        for i in range(10000):
            total += i
        print("total:", total)
    
    timing(works)
    

    執行結果

    [root@localhost ~]# python3 NoDecorator2.py
    Start...
    total: 49995000
    Elapsed time(secs): 0.0006550069992954377
    

    說明:
    匯入時間模組.

    import time
    

    回傳當下時間,格式為 float 浮點數.

    time.perf_counter()
    

    在 Python 中的 function 函數可以當成參數傳遞並執行,透過這種方式可以重覆使用相同的程式碼.

    範例中會去執行 timing 函數並帶 work 函數為參數.

    timing(works)
    

    在 timing 函數的傳入值是 func 函數(這個名稱可以自己喜歡來命名, funabc , test123 …),而實際對應到的就是前面呼叫時所使用的 works 函數.

    def timing(func):
    

    在 timing 函數後面有呼叫 func (即是 works 函數).

        func()
    

使用 Decorator (裝飾器)

  • 範例 : 使用 包裝函數 wrapper 把原本函數包裝起來
    Decorator (裝飾器)是 Python 中的一種設計模式,它允許用戶在不修改其結構的情況下向現有對象添加新功能.裝飾器允許我們 wrap (包裝)另一個函數以擴展包裝函數的行為,而無需永久修改它.

    [root@localhost ~]# vi Decorator1.py
    import time
    def timing(func):
        def wrapper():
            print("Start...")
            t1 = time.perf_counter()
            func()
            t2 = time.perf_counter()
            print("Elapsed time(secs):", t2 - t1)
        return wrapper
    
    def work100():
        total = 0
        for i in range(100):
            total += i
        print("1 to 100 total:", total)
    
    def work10000():
        total = 0
        for i in range(10000):
            total += i
        print("1 to 10000 total:", total)
    
    work1 = timing(work100)
    work1()
    
    print ('----------')
    
    work2 = timing(work10000)
    work2()
    

    執行結果

    [root@localhost ~]# python3 Decorator1.py
    Start...
    1 to 100 total: 4950
    Elapsed time(secs): 1.1469999662949704e-05
    ----------
    Start...
    1 to 10000 total: 49995000
    Elapsed time(secs): 0.00045819100023436476
    

    說明:
    我們在 timing 函數使用了包裝函數 wrapper (名稱自訂,可以為 wrap123 , 123 .. 回傳相同名稱) 把不同函數包裝起來.這就是 Decorator function 裝飾函數.

        def wrapper():
        return wrapper
    

    使用上需要先包裝成新的函數 work1 ,包含 timing 函數與 work100 函數,再去執行 work1 函數.

    work1 = timing(work100)
    work1()
    

    work2 函數一樣是透過包裝的方式把 timing 函數與 work10000 函數包裝 wrap 再一起.

    work2 = timing(work10000)
    work2()
    
  • 範例 : 裝飾器 (decorator) 的 語法糖 (syntax sugar)
    透過 @ 語法糖 (syntax sugar) 可以簡化上面的範例.

    [root@localhost ~]# vi Decorator2.py
    import time
    def timing(func):
        def wrapper():
            print("Start...")
            t1 = time.perf_counter()
            func()
            t2 = time.perf_counter()
            print("Elapsed time(secs):", t2 - t1)
        return wrapper
    
    @timing
    def works():
        total = 0
        for i in range(10000):
            total += i
        print("total:", total)
    
    works()
    

    執行結果

    [root@localhost ~]# python3 Decorator2.py
    Start...
    total: 49995000
    Elapsed time(secs): 0.0006422139999813226
    

    說明:
    其中的 @ 就是 syntax sugar , 這個動作等同下面,在呼叫 works 涵式前會先傳入 timing 涵式,並把回傳的涵式命明為同名的 works 函數.

    @timing
    def works():
    
    works = timing(works)
    

    當有多個 syntax sugar 執行的順序將會 由下而上,下面 b 函數會先執行 @a3 然後 @a2 最後才是 @a1.

    @a1
    @a2
    @a3
    def b():
    

有參數的 Function (函數) 與 Decorator (裝飾器)

  • 範例 : 有參數的函數
    [root@localhost ~]# vi Decorator3.py
    import time
    def timing(func):
        def wrapper(*args, **kwargs):
            print("Start...")
            t1 = time.perf_counter()
            func(*args, **kwargs)
            t2 = time.perf_counter()
            print("Elapsed time(secs):", t2 - t1)
        return wrapper
    
    @timing
    def works(start, stop):
        total = 0
        for i in range(start, stop):
            total += i
        print("total:", total)
    
    works(1, 10000)
    

    執行結果

    [root@localhost ~]# python3 Decorator3.py
    Start...
    total: 49995000
    Elapsed time(secs): 0.0005442179999590735
    

    說明:
    *args 和 **kwargs 表示為任意數量的參數.

    (*args, **kwargs)
    

    再把這些參數轉傳到被包裝的函數.

            func(*args, **kwargs)
    
  • 範例 : 有參數的函數 (使用 functools 的 wraps)
    上面的範例會有一個問題(回傳的函式名稱不是原本的函式),檢視一下 works(在 Decorator3.py 最後面加上以下) .

    print(works)
    

    執行結果,明明是 works 卻顯示為 timing 函式.

    [root@localhost ~]# python3 Decorator3.py
    <function timing.<locals>.wrapper at 0x7f5b4fdb0840>
    

    這是因為裝飾詞在被 wrap 之後 __name__ 屬性就會變成 wrap , 可以已透 python 內建的 functools 的 wraps 避免這個問題.

    [root@localhost ~]# vi Decorator4.py
    import time
    from functools import wraps
    
    def timing(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print("Start...")
            t1 = time.perf_counter()
            func(*args, **kwargs)
            t2 = time.perf_counter()
            print("Elapsed time(secs):", t2 - t1)
        return wrapper
    
    @timing
    def works(start, stop):
        total = 0
        for i in range(start, stop):
            total += i
        print("total:", total)
    
    print(works)
    

    執行結果

    [root@localhost ~]# python3 Decorator4.py
    <function works at 0x7faae736d1e0>
    

    說明:
    跟前面程式差別只有以下兩行.
    從 functools 匯入 wraps

    from functools import wraps
    

    避免 wrap 之後 __name__ 屬性就會變成 wrap 這個問題.

        @wraps(func)
    
  • 範例 : 有參數的 Decorator (裝飾器)
    [root@localhost ~]# vi Decorator5.py
    import time
    def timing(times):
        def outer(func):    
            def wrapper(*args, **kwargs):
                print("Start...")
                t1 = time.perf_counter()
                for i in range(times):
                    func(*args, **kwargs)
                t2 = time.perf_counter()
                print("Elapsed time(secs):", t2 - t1)
            return wrapper
        return outer
    
    @timing(2)
    def works(start, stop):
        total = 0
        for i in range(start, stop):
            total += i
        print("total:", total)
    
    works(1, 10000)
    

    執行結果

    [root@localhost ~]# python3 Decorator5.py
    Start...
    total: 49995000
    total: 49995000
    Elapsed time(secs): 0.0010434840005473234
    

    說明:
    需要參數的裝飾器需要再使用一層函式包裝原先的裝飾器

    def timing(times):
        def outer(func):    
            def wrapper(*args, **kwargs):
    

    依據 @timing(2) 傳入 def timing(times): 來指定執行 func (works 函數) 的次數

                for i in range(times):
                    func(*args, **kwargs)
    
沒有解決問題,試試搜尋本站其他內容

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料