本文始發於我的公衆號:TechFlow,原創不易,求個關注程序員
今天是Python專題的第12篇文章,咱們來看看Python裝飾器。web
差很少五年前面試的時候,我就領教過它的重要性。那時候我Python剛剛初學乍練,看完了廖雪峯大神的博客,就去面試了。我應聘的並非一個Python的開發崗位,可是JD當中寫到了須要熟悉Python。我看網上的面經說到Python常常會問裝飾器,我當時想的是裝飾器我已經看過了,應該問題不大……面試
沒想到面試的時候還真的問到了,面試官問我Python當中的裝飾器是什麼。因爲緊張和遺忘,我支支吾吾了半天也沒答上來。我隱約聽到了電話那頭的一聲嘆息……編程
時隔多年,我已經不記得那是一傢什麼公司了(估計規模也不大),但裝飾器很重要這個事情給我深深打下了烙印。app
現在若是再有面試官問我Python中的裝飾器是什麼,我一句話就能給回答了,倒不是我裝逼,實際上也的確只須要一句話。Python中的裝飾器,本質上就是一個高階函數。編輯器
你可能不太清楚高階函數的定義,不要緊,咱們能夠類比一下。在數學當中高階導數,好比二次導數,表示導數的導數。那麼這裏高階函數天然就是函數的函數,結合咱們以前介紹過的函數式編程,也就是說是一個返回值是函數的函數。可是這個定義是充分沒必要要的,也就是說裝飾器是高階函數,可是高階函數並不都是裝飾器。裝飾器是高階函數一種特殊的用法。函數式編程
在介紹裝飾器的具體使用以前,咱們先來了解和熟悉一下Python當中的任意參數。函數
Python當中支持任意參數,它寫成*args, **kw。表示的含義是接受任何形式的參數。單元測試
舉個例子,好比咱們定義一個函數:測試
def exp(a, b, c='3', d='f'):
print(a, b, c, d)
複製代碼
咱們能夠這樣調用:
args = [1, 3]
dt = {'c': 4, 'd': 5}
exp(*args, **dt)
複製代碼
最後輸出的結果是1, 3, 4, 5。也就是說咱們用一個list和dict能夠表示任何參數。由於Python當中規定必選參數必定寫在可選參數的前面,而必選參數是能夠不用加上名稱標識的,也就是能夠不用寫a=1,直接傳入1便可。那麼這些沒有名稱標識的必選參數就能夠用一個list來表示,而可選參數是必需要加上名稱標識的,這些參數能夠用dict來表示,這二者相加能夠表示任何形式的參數。
注意咱們傳入list和dict的時候前面加上了*和**,它表示將list和dict當中的全部值展開。若是不加的話,list和dict會被當成是總體傳入。
因此若是一個函數寫成這樣,它表示能夠接受任何形式的參數。
def exp(*args, **kw):
pass
複製代碼
明白了任意參數的寫法以後,裝飾器就不難了。
既然咱們能夠用*args, **kw接受任何參數。而且Python當中支持一個函數做爲參數傳入另一個函數,若是咱們把函數和這個函數的全部參數所有傳入另一個函數,那麼不就能夠實現代理了嗎?
仍是剛纔的例子,咱們額外增長一個函數:
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)
複製代碼
裝飾器的本質其實就是這樣一個agent函數,可是若是使用的時候須要手動傳入會很是麻煩,使用起來不太方便。因此Python當中提供了特定的庫,咱們可讓裝飾器以註解的方式使用,大大簡化操做:
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)
複製代碼
在這個例子當中,咱們定義了一個wrapexp的裝飾器。咱們在其中的wrapper方法當中實現了裝飾器的邏輯,wrapexp當中傳入的參數func是一個函數,wrapper當中的參數則是func的參數。因此咱們在wrapper當中調用func(*args, **kw),就是調用打上了這個註解的函數自己。好比在這個例子當中,咱們沒有作任何事情,只是在原樣調用以前多輸出了一行’this is a wrapper',表示咱們的裝飾器調用成功了。
咱們理解了裝飾器的基本使用方法以後,天然而然地會問一個自然的問題,學會了它究竟有什麼用呢?
若是你從上面的例子當中沒有領會到裝飾器的強大,不如讓我用一個例子再來暗示一下。好比說你是一個程序員,辛辛苦苦作出了一個功能,寫了好幾千行代碼,上百個函數,終於經過了審覈上線了。這個時候,你的產品經理找到了你說,通過分析咱們發現上線的功能運行速度不達標,常常有請求超時,你能不能計算一下每一個函數運行的耗時,方便咱們找到須要優化的地方?
這是一個很是合理的請求,但想一想看你寫了上百個函數,若是每個函數都要手動添加時間計算,這要寫多少代碼?萬一哪一個函數不當心改錯了,你又得一一檢查,而且若是要求嚴格的話你還得爲每個函數專門寫一個單元測試……
我想,正常的程序員應該都會抗拒這個需求。
可是有了裝飾器就很簡單了,咱們能夠實現一個計算函數耗時的裝飾器,而後咱們只須要給每個函數加上註解就行了。
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
複製代碼
這也是裝飾器最大的用途,能夠在不修改函數內部代碼的前提下,爲它包裝一些額外的功能。
咱們以前說過裝飾器的本質是高階函數,因此咱們也能夠和高階函數同樣來調用裝飾器,好比下面這樣:
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)
複製代碼
這樣的方式獲得的結果和使用註解是同樣的,也就是說咱們加上註解的本質其實就是調用裝飾器返回一個新的函數。
既然和高階函數是同樣的,那麼就帶來了一個問題,咱們使用的其實已經再也不是原函數了,而是一個由裝飾器返回的新函數,雖然這個函數的功能和原函數同樣,可是一些基礎的信息其實已經丟失了。
好比咱們能夠打印出函數的name來作個實驗:
正常的函數調用__name__返回的都是函數的名稱,可是當咱們加上了裝飾器的註解以後,就會發生變化,一樣,咱們輸出加上了裝飾器註解以後的結果:
咱們會發現輸出的結果變成了wrapper,這是由於咱們實現的裝飾器內部的函數叫作wrapper。不只僅是__name__,函數內部還有不少其餘的基本信息,好比記錄函數內描述的__doc__,__annotations__等等,這些基本信息被稱爲是元信息,這些元信息因爲咱們使用註解發生了丟失。
有沒有什麼辦法能夠保留這些函數的元信息呢?
其實很簡單,Python當中爲咱們提供了一個專門的裝飾器用來保留函數的元信息,咱們只須要在實現裝飾器的wrapper函數當中加上一個註解wraps便可。
def wrapexp(func):
@wraps(func)
def wrapper(*args, **kwargs):
print('this is a wrapper')
func(*args, **kwargs)
return wrapper
複製代碼
加上了這個註解以後,咱們再來檢查函數的元信息,會發現它和咱們預期一致了。
瞭解了Python中的裝飾器以後,再來看以前咱們用過的@property, @staticmethod等註解,想必都能明白,它們背後的實現其實也是裝飾器。靈活使用裝飾器能夠大大簡化咱們的代碼,讓咱們的代碼更加規範簡潔,還能靈活地實現一些特殊的功能。
裝飾器的用法不少,今天介紹的只是其中最基本的,在後續的文章當中,還會繼續和你們分享它更多其餘的用法。在文章開始的時候我也說了,裝飾器是Python進階必學的技能之一。想要熟練掌握這門語言,靈活運用,看懂大佬的源碼,裝飾器是必須會的東西。
但願你們都能有所收穫,原創不易,厚顏求個贊和關注~