一篇文章搞定Python裝飾器,看完就算面試也不再慌


一篇文章搞定Python裝飾器,看完就算面試也不再慌


裝飾器本質

如今如果再有面試官問我Python中的裝飾器是什麼,我一句話就能給回答了,倒不是我裝逼,實際上也的確只需要一句話。Python中的裝飾器,本質上就是一個高階函數

你可能不太清楚高階函數的定義,沒關係,我們可以類比一下。在數學當中高階導數,比如二次導數,表示導數的導數。那麼這裡高階函數自然就是函數的函數,結合我們之前介紹過的函數式編程,也就是說是一個返回值是函數的函數。但是這個定義是充分不必要的,也就是說裝飾器是高階函數,但是高階函數並不都是裝飾器。裝飾器是高階函數一種特殊的用法。


任意參數

在介紹裝飾器的具體使用之前,我們先來了解和熟悉一下Python當中的任意參數。

Python當中支持任意參數,它寫成*args, **kw。表示的含義是接受任何形式的參數。

舉個例子,比如我們定義一個函數:

<code>def exp(a, b, c='3', d='f'):
    print(a, b, c, d)/<code>

我們可以這樣調用:

<code>args = [1, 3]
dt = {'c': 4, 'd': 5}

exp(*args, **dt)/<code>

最後輸出的結果是1, 3, 4, 5。也就是說我們用一個list和dict可以表示任何參數。因為Python當中規定必選參數一定寫在可選參數的前面,而必選參數是可以不用加上名稱標識的,也就是可以不用寫a=1,直接傳入1即可。那麼這些沒有名稱標識的必選參數就可以用一個list來表示,而可選參數是必須要加上名稱標識的,這些參數可以用dict來表示,這兩者相加可以表示任何形式的參數。

注意我們傳入list和dict的時候前面加上了*和**,它表示將list和dict當中的所有值展開。如果不加的話,list和dict會被當成是整體傳入。

所以如果一個函數寫成這樣,它表示可以接受任何形式的參數。

<code>def exp(*args, **kw):
    pass/<code>

定義裝飾器

明白了任意參數的寫法之後,裝飾器就不難了。

既然我們可以用*args, **kw接受任何參數。並且Python當中支持一個函數作為參數傳入另外一個函數,如果我們把函數和這個函數的所有參數全部傳入另外一個函數,那麼不就可以實現代理了嗎?

還是剛才的例子,我們額外增加一個函數:

<code>def exp(a, b, c='3', d='f'):
    print(a, b, c, d)

def agent(func, *args, **kwargs):
    func(*args, **kwargs)

args = [1]
dt = {'b': 1, 'c': 4, 'd': 5}

agent(exp, *args, **dt)
/<code>

裝飾器的本質其實就是這樣一個agent的函數,但是如果使用的時候需要手動傳入會非常麻煩,使用起來不太方便。所以Python當中提供了特定的庫,我們可以讓裝飾器以註解的方式使用,大大簡化操作:


<code>from functools import wraps

def wrapexp(func):
    def wrapper(*args, **kwargs):
        print('this is a wrapper')
        func(*args, **kwargs)
    return wrapper


@wrapexp
def exp(a, b, c='3', d='f'):
    print(a, b, c, d)


args = [1, 3]
dt = {'c': 4, 'd': 5}

exp(*args, **dt)/<code>

在這個例子當中,我們定義了一個wrapexp的裝飾器。我們在其中的wrapper方法當中實現了裝飾器的邏輯

,wrapexp當中傳入的參數func是一個函數,wrapper當中的參數則是func的參數。所以我們在wrapper當中調用func(*args, **kw),就是調用打上了這個註解的函數本身。比如在這個例子當中,我們沒有做任何事情,只是在原樣調用之前多輸出了一行’this is a wrapper',表示我們的裝飾器調用成功了。

裝飾器用途


我們理解了裝飾器的基本使用方法之後,自然而然地會問一個天然的問題,學會了它究竟有什麼用呢?

如果你從上面的例子當中沒有領會到裝飾器的強大,不如讓我用一個例子再來暗示一下。比如說你是一個程序員,辛辛苦苦做出了一個功能,寫了好幾千行代碼,上百個函數,終於通過了審核上線了。這個時候,你的產品經理找到了你說,經過分析我們發現上線的功能運行速度不達標,經常有請求超時,你能不能計算一下每個函數運行的耗時,方便我們找到需要優化的地方?

這是一個非常合理的請求,但想想看你寫了上百個函數,如果每一個函數都要手動添加時間計算,這要寫多少代碼?萬一哪個函數不小心改錯了,你又得一一檢查,並且如果要求嚴格的話你還得為每一個函數專門寫一個單元測試……

我想,正常的程序員應該都會抗拒這個需求。

但是有了裝飾器就很簡單了,我們可以實現一個計算函數耗時的裝飾器,然後我們只需要給每一個函數加上註解就好了。

<code>import time
from functools import wraps
def timethis(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name__, end-start)
        return result
    return wrapper
/<code>

這也是裝飾器最大的用途,可以在不修改函數內部代碼的前提下,為它包裝一些額外的功能


元信息


我們之前說過裝飾器的本質是高階函數,所以我們也可以和高階函數一樣來調用裝飾器,比如下面這樣:

<code>def exp(a, b, c='3', d='f'):
    print(a, b, c, d)


args = [1, 3]
dt = {'c': 4, 'd': 5}

f = wrapexp(exp)
f(*args, **dt)/<code>

這樣的方式得到的結果和使用註解是一樣的,也就是說我們加上註解的本質其實就是調用裝飾器返回一個新的函數。

既然和高階函數是一樣的,那麼就帶來了一個問題,我們使用的其實已經不再是原函數了,而是一個由裝飾器返回的新函數,雖然這個函數的功能和原函數一樣,但是一些基礎的信息其實已經丟失了。

比如我們可以打印出函數的name來做個實驗:

一篇文章搞定Python裝飾器,看完就算面試也不再慌

正常的函數調用__name__返回的都是函數的名稱,但是當我們加上了裝飾器的註解之後,就會發生變化,同樣,我們輸出加上了裝飾器註解之後的結果:

一篇文章搞定Python裝飾器,看完就算面試也不再慌

我們會發現輸出的結果變成了wrapper,這是因為我們實現的裝飾器內部的函數叫做wrapper。不僅僅是__name__,函數內部還有很多其他的基本信息,比如記錄函數內描述的__doc__,__annotations__等等,這些基本信息被稱為是元信息

,這些元信息由於我們使用註解發生了丟失。

有沒有什麼辦法可以保留這些函數的元信息呢?

其實很簡單,Python當中為我們提供了一個專門的裝飾器器用來保留函數的元信息,我們只需要在實現裝飾器的wrapper函數當中加上一個註解wraps即可。

<code>def wrapexp(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print('this is a wrapper')
        func(*args, **kwargs)
    return wrapper/<code>

加上了這個註解之後,我們再來檢查函數的元信息,會發現它和我們預期一致了。

一篇文章搞定Python裝飾器,看完就算面試也不再慌


總結


瞭解了Python中的裝飾器之後,再來看之前我們用過的@property, @staticmethod等註解,想必都能明白,它們背後的實現其實也是裝飾器。靈活使用裝飾器可以大大簡化我們的代碼, 讓我們的代碼更加規範簡潔,還能靈活地實現一些特殊的功能。

裝飾器的用法很多,今天介紹的只是其中最基本的,在後續的文章當中,還會繼續和大家分享它更多其他的用法。在文章開始的時候我也說了,裝飾器是Python進階必學的技能之一。想要熟練掌握這門語言,靈活運用,看懂大佬的源碼,裝飾器是必須會的東西。


分享到:


相關文章: