Python裝飾器不會傳參?彆著急,這篇文章為你解惑

今天是Python專題的第13篇文章,上一篇文章當中我們介紹了Python裝飾器的定義和基本的用法,這篇文章我們一起來學習一下Python裝飾器的一些進階使用方法。對裝飾器不太熟悉,或者錯過了上篇內容的小夥伴可以點擊下方傳送門。


一文搞定Python裝飾器,看完面試不再慌


之前的文章當中我們從前到後仔細推到了一下裝飾器的本質和用途,也學會了它的基本用法,已經足夠應付80%的場景了。但是總有20%的場景使用基本的方法解決不了,這個時候就需要我們學習更多、更全的其他用法。


比如我想要通過一個參數控制裝飾器的功能,這個問題其實很常見。就拿記錄時間來說,我們都知道時間可以記錄成很多種格式,比如可以記成2020-05-04也可以記錄成20200504,還可以記錄成04/05/2020,如果是後端還會記錄時間的時間戳。比如說我們現在實現了一個記錄日誌的裝飾器,用來給我們的方法打上日誌,現在我們想要控制記錄日誌的時候打印出來的時間格式,這個需求使用最簡單的裝飾器就沒有辦法解決了。


這個時候,如果想要解決問題,就必須引入參數,也就是說我們必須要在裝飾器當中加入參數才行。但問題來了,這個參數怎麼加,加在哪裡呢?


定義裝飾器參數


在我們介紹具體的用法之前,我們先來回顧一下裝飾器的代碼:

<code>def mydec(func):
    @wraps(func)
    def mywrap(*args, **kw):
        print('hello this is decorator1')
        func(*args, **kw)
    return mywrap
    
@mydec
def helloWorld():
    print('hello, world')/<code>


這個就是我們上次講的最簡單的那種裝飾器,假如說我們這個時候希望傳入一個參數type,可以控制裝飾器的輸出結果。就像這樣:

<code>@mydec(type_='test')
def helloWorld():
    print('hello, world')/<code>


我們可能會想是不是應該在mydec這個方法的參數裡面加上一個type_,但是如果你試一下就好發現這樣是不行的,會得到一個error:

Python裝飾器不會傳參?彆著急,這篇文章為你解惑


Error錯誤的字面意思很好理解,但是原因卻令人費解。這個Error是說函數mydec少了一個必選參數func,這個func就是我們要包裝的函數,但是這個不是自動傳入的嗎,怎麼會提示我們少了這個參數呢?


如果這個問題的本質不能理解的話,那麼裝飾器就很難大成了,因為只有理解清楚了這一點,才能理解後面裝飾器各種稀奇古怪的進階用法。但是很坑爹的是,很多資料當中都只是簡單地介紹了怎麼用,很少會探究其中背後的原因,這會讓初學者在學習的時候陷入費解。我在學習的時候也花了很多心思,才終於搞明白,說穿了很簡單,但是想通不容易。


其實這樣會報錯的主要原因是註解當中有參數和沒有參數的裝飾器是完全不同的。


我們來回顧一下不加參數的裝飾器的用法,比如:

<code>@mydec
def hello_world():
    pass/<code>


我們執行hello_world()的時候,等價於執行mydec(hello_world)()。看明白了嗎,我們把這行代碼展開,它其實是下面這兩行代碼共同執行的結果:

<code>cur = mydec(hello_world)
cur()/<code>


如果hello_world這個函數帶上參數呢?

<code>@mydec
def hello_world(*args, **kw):
    pass/<code>


那麼執行的時候它其實是這樣的:

<code>cur = mydec(hello_world)
cur(*args, **kw)/<code>


這個理解了之後,我們繼續往下,現在我們想要將一個參數傳給裝飾器,按照我們的想法下面這兩段代碼應該是一樣的。

<code>@mydec(type_='test')
def helloWorld():
    print('hello, world')


cur = mydec(hello_world, type_)
cur()/<code>


但是很遺憾的是,Python解釋器當中並不是這麼設計的。它對加上了參數的裝飾器

多做了一層封裝,也就是說上面傳入參數的hello_world函數執行的時候等價於下面這段代碼:

<code>cur1 = mydec(type_)
cur2 = cur1(hello_world)
cur2()/<code>


正是因為額外多封裝了一層,所以函數和裝飾器的參數傳入裝飾器的順序是不同的,順序也是不一樣的。明白了這點之後就簡單很多了,既然Python解釋器在解釋裝飾器參數的時候多增加了一層,那麼如果我們想要實現帶參數的裝飾器,只需要也在裝飾器當中多封裝一層就可以了。比如可以寫成這樣:

<code>def mydec(type_=None):
    def decorate(func):
        @wraps(func)
        def mywrap():
            if type_ is not None:
                print(type_)
            func()
        return mywrap
    return decorate/<code>


這樣我們再執行就可以了:

Python裝飾器不會傳參?彆著急,這篇文章為你解惑


默認參數怎麼辦


到這裡看似一切都很完美,但其實有一個很大的問題被我們忽略了。


這個問題就是默認參數問題,在前面我們定義裝飾器的時候,將type_這個參數設置成了可選的。這也很符合我們實際情況,如非必要,參數能省略就省略。但是這就導致了一個問題,對於不用加上參數的裝飾器,有些人習慣寫成mydec(),有些人習慣寫成mydec。如果我們試一下mydec,就會發現這樣寫會報錯:

Python裝飾器不會傳參?彆著急,這篇文章為你解惑


這個報錯和上面的報錯一模一樣,出現的原因也是一樣的,都是少了func參數。但是很奇怪啊,為什麼會少了func呢?


原因很簡單,因為我們把括號去掉,裝飾器又回到了之前的兩層結構!

<code>cur = mydec(hello_world)
cur(*args, **kw)/<code>


這就很坑爹了,我們裝飾器的結構肯定是不能改變的,如果使用兩層結構就沒辦法傳入參數了,但是如果不傳參的時候怎麼辦,難道就只能強制程序員統一風格全部加上括號嗎?這當然也是一個辦法,那還有沒有更好的辦法呢?有沒有辦法統一這兩種邏輯呢?


當然是有的,為了解決這個問題,我們需要用到一個新的工具,叫做偏函數


偏函數很好理解,它本意也是一個高階函數,其實就是閉包。偏函數的使用場景針對多參數的函數,通過使用偏函數,可以固定若干個參數的傳值,從而起到簡化函數傳參的作用。我們來看一個例子,我們創建一個pow函數,用來計算x的n次方:

<code>import math
def pow(x, n):
    return math.pow(x, n)/<code>


這個函數需要傳入x和n兩個參數,如果我們當前只需要計算平方,我們可以使用閉包,固定其中的參數n,生成一個新的函數來做到這點。比如:

<code>def mypow(n):
    def func(x):
        return pow(x, n) 
    return func/<code>
Python裝飾器不會傳參?彆著急,這篇文章為你解惑


偏函數的本質就是這樣一個閉包,只不過它簡化了我們的代碼而已:

<code>from functools import partial

pow2 = partial(pow, n=2)
pow2(6)/<code>


使用偏函數我們只需要傳入待加工的原函數,以及固定的參數值即可。我們把偏函數用在裝飾器當中,就可以解決剛才的問題。回憶一下,不帶參數的裝飾器是兩層函數嵌套,而帶上參數的是三層嵌套。那麼我們使用partial,專門為帶上參數的情況額外增加一層嵌套即可:

<code>def mydec(func=None, type_=None):
    # 不帶參數的話,func會是None,這時候我們固定參數即可
    if func is None:
        return partial(mydec, type_=type_)
    
    @wraps(func)
    def mywrap():
        if type_ is not None:
            print(type_)
        func()
    return mywrap/<code>


我們來看下這其中的細節,當我們不傳入參數的時候,我們其實執行的是cur = mydec(func),這個時候func不為空,那麼不會觸發if中的語句,所以會直接返回mywrap。如果傳入參數,這時候func是None,會觸發if中的partial。注意這裡我們在partial當中傳入的函數依然是mydec,也就是說我們固定了type_這個參數,調用的話依然返回的是mywrap,相當於我們通過partial額外在兩層結構當中專門為帶參數的情況增加了一層,統一了邏輯。


結尾


今天的概念比之前的裝飾器要複雜很多,一時可能並不好理解,其實這是非常正常的。這不僅僅是裝飾器的問題,也不僅是Python的問題,歸根結底這是函數式編程的特性導致的。函數式編程的優點就是高度靈活,使用非常方便,但缺點也很明顯,代碼難以維護,閱讀難度高,理解起來也不簡單。典型的初學簡單,精深非常難的典型。所以如果大家覺得一時理解不了,這並不是你們的問題,一方面我們需要培養自己函數傳編程的思維,另一方面我們也需要熟悉Python中裝飾器的使用方法。


最後說點題外話,由於只狼和仁王,最近有點迷上了硬核遊戲。剛開始玩的時候,覺得非常困難,經常卡關,一個boss死個幾十次是家常便飯。等到了後來,慢慢找到了訣竅,瞬間發現這類遊戲甚至所有遊戲都變得簡單了。


這不僅僅是我熟悉了,更多的是因為玩遊戲的時候也開始思考了,開始思考這些boss設計了哪些招數?設計者給我們留下了哪些操作的空間對付它?有哪些規律可循?思考的多了,訣竅也就有了。打多了之後,很多boss就只剩下了初見難,只要打個兩三次熟悉了套路,就可以過關了。慢慢地我發現生活當中的很多事情其實和遊戲中的boss一樣,只是初見難,第一次見到的時候覺得無從下手,覺得難以理解,覺得龐然大物,所以很難。但只要有一顆堅毅、勇敢的心,學會冷靜理智去分析,其實不過只是紙老虎而已。


希望能給大家一點小小的啟發,希望大家面前的困難都只是紙老虎,希望大家都能找到自己的勇氣。


今天的文章就到這裡,原創不易,需要你的一個關注,你的舉手之勞對我來說很重要。


分享到:


相關文章: