草根學Python(十六) 裝飾器(逐步演化成裝飾器)

兩點水發表於2018-01-11

上一篇文章將通過解決一個需求問題來了解了閉包,本文也將一樣,通過慢慢演變一個需求,一步一步來了解 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
暱稱:兩點水  部門:做鴨事業部 上班打卡成功
複製程式碼

通過程式碼,能知道裝飾器函式一般做這三件事:

  1. 接收一個函式作為引數
  2. 巢狀一個包裝函式, 包裝函式會接收原函式的相同引數,並執行原函式,且還會執行附加功能
  3. 返回巢狀函式

可是,認真一看這程式碼,這裝飾器的寫法怎麼比函數語言程式設計還麻煩啊。而且看起來比較複雜,甚至有點多此一舉的感覺。

那是因為我們還沒有用到裝飾器的 “語法糖” ,我們看上面的程式碼可以知道, 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 入門系列文章結束了。如果感興趣的話,可以關注微信公眾號,回覆 “Python” 獲取更多的 Python 學習資料。

微信公眾號

相關文章