iT邦幫忙

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

30天python雜談系列 第 30

decorator與closure雜談之四———感言與真‧decorator介紹2

來個感言先~

好不容易到第30天了,還不先灑個花花~

是說這個鐵人賽雖然只有30天,但對我來說其實有60天阿QQ,從開賽前一個月就開始構思,這60天大部分的休閒時間都在不停的查資料、整理思緒、寫文章,然後公司年底又有發表會,真的是忙得不可開交,在過程中一直感覺到別人宅在家還可以玩遊戲看漫畫,幹我在寫鐵人賽阿QQ,把自己搞得很孤僻,一直有莫名的厭世感,每次寫文章前都要懷疑人生...

但60天走過來還是是覺得挺值得的,至少從原本在寫第1篇文章時都要想很久才下筆到現在已經可以邊寫邊整理思緒,對於寫技術文章已經有一種稀鬆平常的感覺了,而且對python的認識也增進很多,雖然這樣回過頭來看自己的文章還是有不夠精簡的缺點,但和兩個月前相比,表達能力應該有進步很多...了吧哈哈。

其實這python雜談系列我還有準備很多材料,只是礙於能力不夠所以最後就只挑了幾個主題來寫,沒寫的主題大概像是GIL、平行處理、和C語言的結合與比較等等,之後應該也不會寫了,但還有一個講class的主題我有答應我朋友要寫,應該會在賽後以龜速補完XD。

至於為什麼以後不會再寫是因為我對於概念類的文章真的寫到很厭倦XD,除非是真的很重要很深刻的概念不然我以後應該會寫應用類的文章,你想想為了好好的寫一篇概念文章,要想一個好的切入點,之後要從各個資料中整理自己的看法以免抄襲,還要仔細用程式檢驗自己的論點是不是對的,真的很麻煩,像是版本差異雜談跟import雜談真的耗費不少心力(但不是很想承認import雜談根本快變抄襲了XD),重點是很多東西寫了你不一定用的到,感覺就是在為了寫文章而消化知識,因為無法學以致用而產生的厭世感其實是我寫鐵人賽時的最大阻力。

現在來看看鐵人賽最初的標語:"今年 讓我們做一件偉大的事",若是能以犧牲來作為偉不偉大的判斷,那我這60天還真是偉大哈哈XD,但若是以提供的效益大小來評斷偉大的話,我發現我所做的只是為巨人的肩膀多貢獻極微薄的一點心力而已,而這也讓我了解到那些真的偉大的人事物是要靠多大多大的努力來完成,是一個我無法想像的scale,在此為我短暫人生所認識的偉大事物致上一份深深敬意。

只是雖然我並沒有做什麼偉大的事,但我們確實做了一件偉大的事,現在看看文章統計,挖,6000篇左右,在一兩個月內有這種產量也是很驚人拉,其中我也看到很多讓我收穫不少的文章,我也在這為來比這個賽的大家致上一份深深敬意。

廢話講太多,最後私心希望我寫的文章對某些人是有用的,趕快把剩下的decorator講一講然後去趕真正的最後一邊文章import_hook。

python decorator與closure雜談之四

昨天我們已經讓我們的計數器decorator變得更有彈性,可以對任意輸入參數與回傳值的初始函式做裝飾,也能夠保有這個初始函式的內部性質,現在我們還想讓這個裝飾器也可以接受輸入參數,這個參數指定我們的裝飾器要用什麼單位來顯示我們的時間,比如說'hour','min'或是'sec',而實現的方法很簡單,就是建立一個回傳一個裝飾器的函數:

這是原本的decorator:

from functools import wraps
import time

def time_count_decorator(init_func): 

    @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 for_loop(n):
    ''' The docstring of for_loop '''
    for i in range(n):
        pass
    return n

回傳一個裝飾器的函式:

from functools import wraps
import time

def time_count_decorator_setter(time_unit='sec'):

    def time_count_decorator(init_func): 

        @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()

            if time_unit == 'sec':
                time_used = te-ts

            elif time_unit == 'min':
                time_used = (te-ts)/60

            elif time_unit == 'hour':
                time_used = (te-ts)/60/60

            print ("{}'s time consume({}): {}".format(init_func.__name__,time_unit,time_used))
        
            return return_value  

        return time_count

    return time_count_decorator

@time_count_decorator_setter('min') # setter回傳一個time_unit='min'的decorator,然後decorator再裝飾for_loop
def for_loop(n):
    ''' The docstring of for_loop '''
    for i in range(n):
        pass
    return n

不知有沒有比較有sense的讀者發現,這個功能之所以能夠實現,要歸功於python對於閉包的支援,因為閉包將time_unit這個變數儲存起來了,所以裝飾器才能進行輸入參數的彈性設定。

然後因為time_unit設有一個初始值'sec',所以我們也可以不帶參數的使用他,比如說:

@time_count_decorator_setter() 
def for_loop(n):
    ''' The docstring of for_loop '''
    for i in range(n):
        pass
    return n

咦咦等一下,但就算不帶參數,後面還是要加個括號阿,這根本對不細心的開發者不友善阿,因為平時用decorator都不加括號的,萬一忘記加括號不就gg了,而且明明就說是time_count_decorator_setter,setter後面不加參數感覺就不像setter了,這樣可讀性反而降低不少。

換句話說,更為操作友善並能維持可讀性的型式應該像以下這樣:

@time_count_decorator # 不想設置參數的時候
def for_loop(n):
    ''' The docstring of for_loop '''
    for i in range(n):
        pass
    return n

@time_count_decorator('hour') # 想設置參數的時候
def for_loop(n):
    ''' The docstring of for_loop '''
    for i in range(n):
        pass
    return n

這方法當然是有,但就有那麼一點點複雜了,甚至稍微摻入了一點黑魔法的元素,首先我們必須拋棄建構一個回傳裝飾器的函數的方法,因為若完全不帶參數以不加括號的話,這個回傳裝飾器的函數就必須扮演裝飾器的角色了,所以我們必須試著讓裝飾器接收time_unit參數之後還能回傳一個裝飾器,而且這個裝飾器還是他自己!!

雖然可以慢慢帶下去講,但這樣要講很久我沒那麼多時間XD,直接給個我自己的答案(其實是查資料後現學現賣):

from functools import wraps, partial
import time

def time_count_decorator(init_func=None, *, time_unit='sec'):
    if init_func is None:
        return partial(time_count_decorator,time_unit=time_unit)

    @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()

        if time_unit == 'sec':
            time_used = te-ts

        elif time_unit == 'min':
            time_used = (te-ts)/60

        elif time_unit == 'hour':
            time_used = (te-ts)/60/60

        print ("{}'s time consume({}): {}".format(init_func.__name__,time_unit,time_used))
    
        return return_value  

    return time_count

在講解原理之前,先講一下他的裝飾方法:

@time_count_decorator # 不想設置參數的時候
def for_loop(n):
    ''' The docstring of for_loop '''
    for i in range(n):
        pass
    return n

@time_count_decorator(time_unit='hour') # 想設置參數的時候,必須用關鍵字參數設定
def for_loop(n):
    ''' The docstring of for_loop '''
    for i in range(n):
        pass
    return n

當我們想要設置參數的時候必須要用關鍵字參數設定,至於那是什麼,可以估狗或是看看我的版本差異系列六,雖然麻煩了一點,但也不會麻煩到哪裡去,反正你都要寫參數了,而且這樣可讀性也變高許多,權衡之下還是個不錯的方法。

我們新的decorator多import了一個工具叫作partial,這個函式可以讓原有的函式簡化,把某些參數固定住,然後變成接收較少參數的簡化函式,原本time_count_decorator是能接受兩個參數的,而partial(time_count_decorator,time_unit=time_unit)會回傳一個簡化函數,裏面的功能和time_count_decorator一模一樣,但是他只能接收一個參數init_func,因為time_unit已經被固定成'hour'了。

怕有人看不懂partial裏面的time_unit=time_unit,這樣說好了,若是長這樣partial(time_count_decorator,time_unit='hour'),代表time_unit已經被固定成'hour',因此只接收init_func,所以partial裏面的time_unit=time_unit,前者代表你想要固定的參數名稱,也就是time_unit,後者是要用什麼值來固定參數,然後因為後面的time_unit已經被"@time_count_decorator(time_unit='hour')"這行指令設為'hour',所以後者time_unit指的就是一個變數,裏面存的是'hour'。

那time_count_decorator(init_func=None, , time_unit='sec')中間的''符號是什麼意思呢,嘿嘿這是python3的新功能,我在版本差異系列六有說過,他算是一個分界點,位於這個,位於這個分界點後面的參數,必須要用關鍵字參數來設定他,所以我上面的裝飾方法才說:"想設置參數的時候,必須用關鍵字參數設定",因為這樣time_unit才會被正確設定。

好我們來解析一下帶參數跟不帶參數裝飾的內部行為吧:

@time_count_decorator # 不想設置參數的時候
def for_loop(n):
    ''' The docstring of for_loop '''
    for i in range(n):
        pass
    return n

在這裡'@'語法糖進行的指令其實是"for_loop = time_count_decorator(for_loop)",再看看最新版裝飾器的內容,發現init_func不等於None,而time_unit也有一個預設值'sec',好,很可以,就只是很簡單的對for_loop做一個裝飾這樣。

@time_count_decorator(time_unit='hour') # 想設置參數的時候
def for_loop(n):
    ''' The docstring of for_loop '''
    for i in range(n):
        pass
    return n

這就有一點曲折了,當我用了關鍵字參數設定time_unit,代表前面的init_func已經被忽略了,所以init_func is None成立,因此回傳了partial(time_count_decorator,time_unit=time_unit),因為time_unit已經被固定為'hour',然後變成只接收init_func參數,所以相當於是回傳:

def simple_time_count_decorator(init_func=None): # 把time_unit全換成'hour'
    if init_func is None:
        return partial(time_count_decorator,time_unit='hour')

    @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()

        if 'hour' == 'sec':
            time_used = te-ts

        elif 'hour' == 'min':
            time_used = (te-ts)/60

        elif 'hour' == 'hour':
            time_used = (te-ts)/60/60

        print ("{}'s time consume({}): {}".format(init_func.__name__,'hour',time_used))
    
        return return_value  

這相當於還是原本的裝飾器,所以"@time_count_decorator(time_unit='hour')"就是執行"for_loop = simple_time_count_decorator(for_loop)"這行指令的意思。

好了,第30篇結束,但還不能高興,還要趕快回去趕import雜談之五。


上一篇
decorator與closure雜談之三———真‧decorator介紹
系列文
30天python雜談30

尚未有邦友留言

立即登入留言