iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 29
0
Data Technology

30天python雜談系列 第 29

decorator與closure雜談之三———真‧decorator介紹

python decorator與closure雜談之三

前天只是稍微的示範了一下decorator的一個很簡單的使用方式,後來就喇了一堆什麼函數對象阿、python作用域阿還是閉包什麼的,現在終於要來一個真正的decorator介紹了,yayaya。

再貼一下前天的使用範例:

import time

def time_count_wrapper(func):
    def time_count():
        ts = time.time()
        func()
        te = time.time()
        print ("time consume: %f" % (te-ts))

    return time_count


@time_count_wrapper
def test_range():
    for i in range(10000000):
        a = i

@time_count_wrapper
def test_xrange():
    for i in xrange(10000000):
        a = i

test_range()
test_xrange()

前天和昨天稍微闡述了一下decorator內部的閉包結構,完全忘了先解釋一下外面最引人注目的'@'語法糖,這個'@'的用意呢,是要代替某種指令操作的寫法而讓程式碼變得更為簡潔易讀,我們可以稍微還原一下穿上糖衣前的樣子:

def test_range():
    for i in range(10000000):
        a = i
test_range = time_count_wrapper(test_range)

def test_xrange():
    for i in xrange(10000000):
        a = i
test_xrange = time_count_wrapper(test_xrange)

裝飾器為初始函式做些裝飾的方法就是,用參數去接收這個初始函式,然後再回傳裝飾過後的函數來更新初始函式,也就是test_range = time_count_wrapper(test_range)和test_xrange = time_count_wrapper(test_xrange)兩者的作用,所以程式這樣寫是可以讓裝飾器work的。

但是這樣寫所能給予的可讀性是很低的,所以python提供了一個語法糖'@time_count_wrapper',來代替這兩個指令,這樣對於函數是否被裝飾,函數被什麼裝飾器裝飾更能顯的一目了然。

給一個'@'語法糖的通則:

@decorator
def init_func(...):
    ...

他所要替換的指令就是:

init_func = decorator(init_func)

另外我們也可以對一個初始函式作很多很多層裝飾,就像下面的這個型式:

@decorator3
@decorator2
@decorator
def init_func(...):
    ...

而其所替換的指令是:

init_func = decorator(init_func)
init_func = decorator2(init_func)
init_func = decorator3(init_func)

稍微領悟一下就能知道,decorator3是最外層的包裝,而decorator是最內層的包裝。

接下來我們來逐步強化我們的decorator吧,我們上個例子的初始函數test_range、test_xrange並沒有設置任何的回傳值,而且也不接受任何的參數,假設現在要讓我們的計時器decorator變得更有彈性,可以計算既接收參數又有回傳值的function,應該要怎麼做呢?可以先請大家自己想一下再來看答案:

這是原本的decorator:

def time_count_decorator(func): 
    def time_count():
        ts = time.time()
        func()
        te = time.time()
        print ("time consume: %f" % (te-ts))

    return time_count

可接受參數並返回值的decorator:

def time_count_decorator(init_func): 
    def time_count(*pos_args,**kw_args): # 可以接收任何的參數
        ts = time.time()
        return_value = init_func(*pos_args,**kw_args) # 把time_count接收到的參數原封不動送到init_func
        te = time.time()
        print ("time consume: %f" % (te-ts))
    
        return return_value  # 回傳init_func的返回值

    return time_count

相信這是可以理解的,再重述一下decorator的通則:

@time_count_decorator
def example_func(a,b,c): # 隨便定義一個func
    ...

     |
     | 去掉糖衣後的樣子
     |
     V

def example_func(a,b,c):
    ...
example_func = time_count_decorator(example_func)

因為time_count_decorator的回傳直是time_count,所以我們相當於更新了原有的example_func(a,b,c):

def example_func(a,b,c):
    ...

     |
     | 更新後的樣子
     |
     V

def time_count(*pos_args,**kw_args):
    ts = time.time()
    return_value = example_func(*pos_args,**kw_args) # 把time_count接收到的參數原封不動送到init_func
    te = time.time()
    print ("time consume: %f" % (te-ts))

    return return_value

經過裝飾後再呼叫example_func其實就變成了呼叫decorator裏面的那個內層函數,所以接收參數以及回傳值的設定都要從time_count_decorator裏面的time_count下手,因為我們之後呼叫的其實都是time_count了,然後若不懂參數之為何要設定成(*pos_args,**kw_args),可以看估狗或是看我寫的版本差異雜談之六,這是確保time_count能夠接受所有的參數,之後的return_value = init_func(*pos_args,**kw_args)就是把這些參數全傳給初始函式,讓初始函式自己判斷這些傳進來的參數是否正確。

然後再介紹一個稍微進階一點,但是會用decorator的人必須要知道的知識,其實'@'語法糖所代替的那一行指令是有一些蹊蹺的:

init_func = decorator(init_func)

這一行指令可不只是把init_func裏面的指令內容掉換而已,他還換掉了init_func.__name__還有init_func.__doc__,當然還有annotations以及calling signature,但就把重點放在前面兩個吧,先看看以下例子:

def time_count_decorator(init_func): # 上個例子改進後的decorator
    def time_count(*pos_args,**kw_args): 
        ''' The docstring of time_count '''  
        ts = time.time()
        return_value = init_func(*pos_args,**kw_args) 
        te = time.time()
        print ("time consume: %f" % (te-ts))
    
        return return_value  

    return time_count

def add(a,b):
    ''' The docstring of add '''
    return a+b

print(add.__name__) # 輸出add
print(add.__doc__) # 輸出The docstring of add

@time_count_decorator
def add(a,b):
    ''' The docstring of add '''
    return a+b
    
print(add.__name__) # 輸出time_count
print(add.__doc__) # 輸出The docstring of time_count

當被'@'語法糖包裹住,add函式的內部資訊都被替換成了time_count的內部資訊,相信這是大家所不樂見的,__name__屬性常常在紀錄日誌或是在檢測過程中打印到stdout時提供很重要的資訊,假設當測試或運行時add內部出現錯誤,但在日誌或螢幕顯示的卻是"Error in time_count!"而不是"Error in add!",大家肯定都會很納悶。

而__doc__屬性裏面儲存的是說明函式功能的字串,那這個字串在python運行時要怎麼叫出來看呢,答案就是利用help(),相信在查看官方函式的功能時help提供了不少幫助,若是在為函式套用裝飾器時把__doc__也更新掉當然對於運行程式沒什麼問題,但以後使用者或是一起共事的同事想要看你的函數功能時就有困難了。

對於這個問題,python的functools模組提供了很簡便的解決方案:

from functools import wraps

def time_count_decorator(init_func): # 上個例子改進後的decorator

    @wraps(init_func)
    def time_count(*pos_args,**kw_args): 
        ''' The docstring of time_count '''  
        ts = time.time()
        return_value = init_func(*pos_args,**kw_args) 
        te = time.time()
        print ("time consume: %f" % (te-ts))
    
        return return_value  

    return time_count

@time_count_decorator
def add(a,b):
    ''' The docstring of add '''
    return a+b
    
print(add.__name__) # 輸出add
print(add.__doc__) # 輸出The docstring of add

functools模組的wraps也是一個裝飾器,主要是用來儲存init_func的內部資訊,所以套用了wraps,就可以確保初始函式的內部資訊也一併被更新掉。

阿阿阿熬夜趕完了,我還不用感言充個最後一天的文章然後再回去補第26天的import_hook。


上一篇
decorator與closure雜談之二———偷渡專家closure
下一篇
decorator與closure雜談之四———感言與真‧decorator介紹2
系列文
30天python雜談30

尚未有邦友留言

立即登入留言