上一篇文章將透過解決一個需求問題來了解了閉包,本文也將一樣,透過慢慢演變一個需求,一步一步來了解 Python 裝飾器。
首先有這麼一個輸出員工打卡資訊的函數:
def punch(): print('昵称:两点水 部门:做鸭事业部 上班打卡成功') punch()
輸出的結果如下:
昵称:两点水 部门:做鸭事业部 上班打卡成功
然後,產品回饋,不行啊,怎麼上班打卡沒有具體的日期,加上打卡的具體日期吧,這應該很簡單,分分鐘解決囉。好吧,那就直接添加列印日期的程式碼吧,如下:
import time def punch(): print(time.strftime('%Y-%m-%d', time.localtime(time.time()))) print('昵称:两点水 部门:做鸭事业部 上班打卡成功') punch()
輸出結果如下:
2018-01-09 昵称:两点水 部门:做鸭事业部 上班打卡成功
這樣改是可以,可是這樣改是改變了函數的功能結構的,本身這個函數定義的時候就是列印某個員工的資訊和提示打卡成功,現在增加列印日期的程式碼,可能會造成很多程式碼重複的問題。例如,還有一個地方只需要列印員工資訊和打卡成功就行了,不需要日期,那麼你又要重寫一個函數嗎?而且列印目前日期的這個功能方法是經常使用的,是可以作為公共函數給各個模組方法呼叫的。當然,這都是作為一個整體項目來考慮的。
既然是這樣,我們可以使用函數式程式設計來修改這部分的程式碼。因為透過先前的學習,我們知道Python 函數有兩個特點,函數也是一個對象,而且函數裡可以巢狀函數,那麼修改一下程式碼變成下面這個樣子:
import time def punch(): print('昵称:两点水 部门:做鸭事业部 上班打卡成功') def add_time(func): print(time.strftime('%Y-%m-%d', time.localtime(time.time()))) func() add_time(punch)
輸出結果:
2018-01-09 昵称:两点水 部门:做鸭事业部 上班打卡成功
這樣是不是發現,這樣子就沒有改動punch 方法,而且任何需要用到打印當前日期的函數都可以把函數傳進add_time 就可以了,就比如這樣:
import time def punch(): print('昵称:两点水 部门:做鸭事业部 上班打卡成功') def add_time(func): print(time.strftime('%Y-%m-%d', time.localtime(time.time()))) func() def holiday(): print('天气太冷,今天放假') add_time(punch) add_time(holiday)
列印結果:
2018-01-09 昵称:两点水 部门:做鸭事业部 上班打卡成功 2018-01-09 天气太冷,今天放假
使用函數程式設計是不是很方便,但是,我們每次呼叫的時候,我們都必須把原來的函數當作參數傳遞進去,還能不能有更好的實作方式呢?有的,就是本文要介紹的裝飾器,因為裝飾器的寫法其實跟閉包是差不多的,不過沒有了自由變量,那麼這裡直接給出上面那段代碼的裝飾器寫法,來對比一下,裝飾器的寫法和函數式程式設計有啥不同。
import time def decorator(func): def punch(): print(time.strftime('%Y-%m-%d', time.localtime(time.time()))) func() return punch def punch(): print('昵称:两点水 部门:做鸭事业部 上班打卡成功') f = decorator(punch) f()
輸出的結果:
2018-01-09 昵称:两点水 部门:做鸭事业部 上班打卡成功
透過程式碼,能知道裝飾器函數一般做這三件事:
接收一個函數作為參數
巢狀一個包裝函數, 包裝函數會接收原函數的相同參數,並執行原函數,也會執行附加功能
返回巢狀函數
可是,認真一看這程式碼,這裝飾器的寫法怎麼比函數式程式設計還麻煩。而且看起來比較複雜,甚至有點多此一舉的感覺。
那是因為我們還沒用到裝飾器的「語法糖」 ,我們看上面的程式碼可以知道, Python 在引入裝飾器(Decorator) 的時候,沒有引入任何新的語法特性,都是基於函數的語法特性。這也就說明了裝飾器不是 Python 特有的,而是每個語言通用的一種程式設計想法。只不過Python 設計出了@ 語法糖,讓定義裝飾器,把裝飾器調用原函數再把結果賦值為原函數的對象名的過程變得更加簡單,方便,易操作,所以Python 裝飾器的核心可以說就是它的語法糖。
那麼要怎麼使用它的語法糖呢?很簡單,根據上面的寫法寫完裝飾函數後,直接在原來的函數上加上 @ 和裝飾器的函數名稱。如下:
import time def decorator(func): def punch(): print(time.strftime('%Y-%m-%d', time.localtime(time.time()))) func() return punch @decorator def punch(): print('昵称:两点水 部门:做鸭事业部 上班打卡成功') punch()
輸出結果:
2018-01-09 昵称:两点水 部门:做鸭事业部 上班打卡成功
那麼這就很方便了,方便在我們的呼叫上,例如例子中的,使用了裝飾器後,直接在原本的函數加上裝飾器的語法糖就可以了,本函數也無虛任何改變,呼叫的地方也不需修改。
不過這裡一直有個問題,就是輸出打卡訊息的是固定的,那我們需要透過參數來傳遞,裝飾器該怎麼寫呢?裝飾器中的函數可以使用*args 可變參數,可是僅使用*args 是無法完全包含所有參數的情況,例如關鍵字參數就不能了,為了能相容關鍵字參數,我們還需要加上**kwargs 。
因此,裝飾器的最終形式可以寫成這樣:
import time def decorator(func): def punch(*args, **kwargs): print(time.strftime('%Y-%m-%d', time.localtime(time.time()))) func(*args, **kwargs) return punch @decorator def punch(name, department): print('昵称:{0} 部门:{1} 上班打卡成功'.format(name, department)) @decorator def print_args(reason, **kwargs): print(reason) print(kwargs) punch('两点水', '做鸭事业部') print_args('两点水', sex='男', age=99)
輸出結果如下:
2018-01-09 昵称:两点水 部门:做鸭事业部 上班打卡成功 2018-01-09 两点水 {'sex': '男', 'age': 99}
至此,草根學 Python 入門系列文章結束了。