1. 程式人生 > >python裝飾器的簡單理解

python裝飾器的簡單理解

1、裝飾器是什麼?
裝飾器,顧名思義,就是用來“裝飾”的。
它長這個樣:

@func_name

它能裝飾的東西有:函式、類

2,為何需要裝飾器?

先來打個比方,內褲可以用來遮羞,但是到了冬天它沒法為我們防風禦寒,聰明的人們發明了長褲,有了長褲後寶寶再也不冷了,裝飾器就像我們這裡說的長褲,在不影響內褲作用的前提下,給我們的身子提供了保暖的功效。回到主題,其實,裝飾器就是這樣的一個函式,它可以讓其他函式在不需要做任何程式碼變動的前提下增加額外功能,裝飾器的返回值也是一個函式物件。它經常用於有切面需求的場景,比如:插入日誌、效能測試、事務處理、快取、許可權校驗等場景。裝飾器是解決這類問題的絕佳設計,有了裝飾器,我們就可以抽離出大量與函式功能本身無關的雷同程式碼並繼續重用。概括的講,裝飾器的作用就是為已經存在的物件新增額外的功能。

3,裝飾器形式

先來看一個簡單例子:

import logging
def use_logging(func):
    logging.warn("%s is running"%func.__name__)
    func()
def bar():
    print("I am bar")
use_logging(bar)

結果:WARNING:root:bar is running
I am bar

上面的程式不難理解,但是這樣的話,每次都要將一個函式名引數傳遞給use_logging()來執行,而且破壞了原有程式碼的邏輯結構。這時我們可以用裝飾器來解決這個問題。

簡單一點的裝飾器:

import logging
def use_logging(func):
    def wrapper(*args,**kwargs):
        logging.warn("%s is running"%func.__name__)
        return func(*args,**kwargs)
    return wrapper
def bar():
    print("I am bar")
bar=use_logging(bar)
bar()

結果:

為了呼叫多個函式,可在其申明前加糖,語句如下:

import logging
def use_logging(func):
    def wrapper(*args,**kwargs):
        logging.warn("%s is running"%func.__name__)
        return func(*args,**kwargs)
    return wrapper

@use_logging
def foo():
    print("i am foo")
foo()

這樣,一個簡單的裝飾器就算完成了。這樣,我們就提高了程式的可重複利用性,並增加了程式的可讀性。

下面再來引用一下金角大王老師的例子來說明一下,文章很逗,但很容易理解,原地址如下:

你是一家視訊網站的後端開發工程師,你們網站有以下幾個版塊

1

2

3

4

5

6

7

8

9

10

11

def home():

print("---首頁----")

def america():

print("----歐美專區----")

def japan():

print("----日韓專區----")

def henan():

print("----河南專區----")

視訊剛上線初期,為了吸引使用者,你們採取了免費政策,所有視訊免費觀看,迅速吸引了一大批使用者,免費一段時間後,每天巨大的頻寬費用公司承受不了了,所以準備對比較受歡迎的幾個版塊收費,其中包括“歐美” 和 “河南”專區,你拿到這個需求後,想了想,想收費得先讓其進行使用者認證,認證通過後,再判定這個使用者是否是VIP付費會員就可以了,是VIP就讓看,不是VIP就不讓看就行了唄。 你覺得這個需求很是簡單,因為要對多個版塊進行認證,那應該把認證功能提取出來單獨寫個模組,然後每個版塊裡呼叫 就可以了,於是你輕輕的就實現了下面的功能 。

user_status = False #使用者登入了就把這個改成True

def login():

_username = "alex" #假裝這是DB裡存的使用者資訊

_password = "abc!23" #假裝這是DB裡存的使用者資訊

global user_status

if user_status == False:

username = input("user:")

password = input("pasword:")

if username == _username and password == _password:

print("welcome login....")

user_status = True

else:

print("wrong username or password!")

else:

print("使用者已登入,驗證通過...")

def home():

print("---首頁----")

def america():

login() #執行前加上驗證

print("----歐美專區----")

def japan():

print("----日韓專區----")

def henan():

login() #執行前加上驗證

print("----河南專區----")

home()

america()

henan()

此時你信心滿滿的把這個程式碼提交給你的TEAM LEADER稽核,沒成想,沒過5分鐘,程式碼就被打回來了, TEAM LEADER給你反饋是,我現在有很多模組需要加認證模組,你的程式碼雖然實現了功能,但是需要更改需要加認證的各個模組的程式碼,這直接違反了軟體開發中的一個原則“開放-封閉”原則,簡單來說,它規定已經實現的功能程式碼不允許被修改,但可以被擴充套件,即:

  • 封閉:已實現的功能程式碼塊不應該被修改
  • 開放:對現有功能的擴充套件開放

這個原則你還是第一次聽說,我擦,再次感受了自己這個野生程式設計師與正規軍的差距,BUT ANYWAY,老大要求的這個怎麼實現呢?如何在不改原有功能程式碼的情況下加上認證功能呢?你一時想不出思路,只好帶著這個問題回家繼續憋,媳婦不在家,去隔壁老王家串門了,你正好落的清靜,一不小心就想到了解決方案,不改原始碼可以呀,

你師從沙河金角大王時,記得他教過你,高階函式,就是把一個函式當做一個引數傳給另外一個函式,當時大王說,有一天,你會用到它的,沒想到這時這個知識點突然從腦子 裡蹦出來了,我只需要寫個認證方法,每次呼叫 需要驗證的功能 時,直接 把這個功能 的函式名當做一個引數 傳給 我的驗證模組不就行了麼,哈哈,機智如我,如是你啪啪啪改寫了之前的程式碼:

user_status = False #使用者登入了就把這個改成True

def login(func): #把要執行的模組從這裡傳進來

_username = "alex" #假裝這是DB裡存的使用者資訊

_password = "abc!23" #假裝這是DB裡存的使用者資訊

global user_status

if user_status == False:

username = input("user:")

password = input("pasword:")

if username == _username and password == _password:

print("welcome login....")

user_status = True

else:

print("wrong username or password!")

if user_status == True:

func() # 看這裡看這裡,只要驗證通過了,就呼叫相應功能

def home():

print("---首頁----")

def america():

#login() #執行前加上驗證

print("----歐美專區----")

def japan():

print("----日韓專區----")

def henan():

#login() #執行前加上驗證

print("----河南專區----")

home()

login(america) #需要驗證就呼叫 login,把需要驗證的功能 當做一個引數傳給login

# home()

# america()

login(henan)

你很開心,終於實現了老闆的要求,不改變原功能程式碼的前提下,給功能加上了驗證,此時,媳婦回來了,後面還跟著老王,你兩家關係 非常 好,老王經常來串門,老王也是碼農,你跟他分享了你寫的程式碼,興奮的等他看完 誇獎你NB,沒成想,老王看後,並沒有誇你,抱起你的兒子,笑笑說,你這個程式碼還是改改吧, 要不然會被開除的,WHAT? 會開除,明明實現了功能 呀, 老王講,沒錯,你功能 是實現了,但是你又犯了一個大忌,什麼大忌? 

你改變了呼叫方式呀, 想一想,現在沒每個需要認證的模組,都必須呼叫你的login()方法,並把自己的函式名傳給你,人家之前可不是這麼呼叫 的, 試想,如果 有100個模組需要認證,那這100個模組都得更改呼叫方式,這麼多模組肯定不止是一個人寫的,讓每個人再去修改呼叫方式 才能加上認證,你會被罵死的。。。。

你覺得老王說的對,但問題是,如何即不改變原功能程式碼,又不改變原有呼叫方式,還能加上認證呢? 你苦思了一會,還是想不出,老王在逗你的兒子玩,你說,老王呀,快給我點思路 ,實在想不出來,老王背對著你問,

老王:學過匿名函式沒有?

你:學過學過,就是lambda嘛

老王:那lambda與正常函式的區別是什麼?

你:最直接的區別是,正常函式定義時需要寫名字,但lambda不需要

老王:沒錯,那lambda定好後,為了多次呼叫 ,可否也給它命個名?

你:可以呀,可以寫成plus = lambda x:x+1類似這樣,以後再呼叫plus就可以了,但這樣不就失去了lambda的意義了,明明人家叫匿名函式呀,你起了名字有什麼用呢?

老王:我不是要跟你討論它的意義 ,我想通過這個讓你明白一個事實

說著,老王拿起你兒子的畫板,在上面寫了以下程式碼:

def plus(n):

return n+1

plus2 = lambda x:x+1

老王: 上面這兩種寫法是不是代表 同樣的意思?

你:是的

老王:我給lambda x:x+1 起了個名字叫plus2,是不是相當於def plus2(x) ?

你:我擦,你別說,還真是,但老王呀,你想說明什麼呢?

老王: 沒啥,只想告訴你,給函式賦值變數名就像def func_name 是一樣的效果,如下面的plus(n)函式,你呼叫時可以用plus名,還可以再起個其它名字,如

1

2

3

calc = plus

calc(n)

你明白我想傳達什麼意思了麼?

你:。。。。。。。。。。。這。。。。。。嗯 。。。。。不太。。。。明白 。。

老王:。。。。這。。。。。呵呵。。。。。。好吧。。。。,那我在給你點一下,你之前寫的下面這段呼叫 認證的程式碼 

1

2

3

4

5

home()

login(america) #需要驗證就呼叫 login,把需要驗證的功能 當做一個引數傳給login

# home()

# america()

login(henan)

你之所改變了呼叫方式,是因為使用者每次呼叫時需要執行login(henan),類似的。其實稍一改就可以了呀

1

2

3

home()

america = login(america)

henan = login(henan)

這樣你,其它人呼叫henan時,其實相當於呼叫了login(henan), 通過login裡的驗證後,就會自動呼叫henan功能。 

你:我擦,還真是唉。。。,老王,還是你nb。。。不過,等等, 我這樣寫了好,那使用者呼叫時,應該是下面這個樣子

1

2

3

4

5

6

home()

america = login(america) #你在這裡相當於把america這個函式替換了

henan = login(henan)

#那使用者呼叫時依然寫

america()

但問題在於,還不等使用者呼叫 ,你的america = login(america)就會先自己把america執行了呀。。。。,你應該等我使用者呼叫 的時候 再執行才對呀,不信我試給你看。。。

老王:哈哈,你說的沒錯,這樣搞會出現這個問題? 但你想想有沒有解決辦法 呢?

你:我擦,你指的思路呀,大哥。。。我哪知道 下一步怎麼走。。。

老王:算了,估計你也想不出來。。。 學過巢狀函式沒有?

你:yes,然後呢?

老王:想實現一開始你寫的america = login(america)不觸發你函式的執行,只需要在這個login裡面再定義一層函式,第一次呼叫america = login(america)只調用到外層login,這個login雖然會執行,但不會觸發認證了,因為認證的所有程式碼被封裝在login裡層的新定義 的函式裡了,login只返回 裡層函式的函式名,這樣下次再執行america()時, 就會呼叫裡層函式啦。。。

你:。。。。。。什麼? 什麼個意思,我蒙逼了。。。

老王:還是給你看程式碼吧。。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

def login(func): #把要執行的模組從這裡傳進來

def inner():#再定義一層函式

_username = "alex" #假裝這是DB裡存的使用者資訊

_password = "abc!23" #假裝這是DB裡存的使用者資訊

global user_status

if user_status == False:

username = input("user:")

password = input("pasword:")

if username == _username and password == _password:

print("welcome login....")

user_status = True

else:

print("wrong username or password!")

if user_status == True:

func() # 看這裡看這裡,只要驗證通過了,就呼叫相應功能

return inner #使用者呼叫login時,只會返回inner的記憶體地址,下次再呼叫時加上()才會執行inner函式

此時你仔細著了老王寫的程式碼 ,感覺老王真不是一般人呀,連這種奇淫巧技都能想出來。。。,心中默默感謝上天賜你一個大牛鄰居。

你: 老王呀,你這個姿勢很nb呀,你獨創的?

此時你媳婦噗嗤的笑出聲來,你也不知道 她笑個球。。。

老王:呵呵, 這不是我獨創的呀當然 ,這是開發中一個常用的玩法,叫語法糖,官方名稱“裝飾器”,其實上面的寫法,還可以更簡單

可以把下面程式碼去掉

1

america = login(america) #你在這裡相當於把america這個函式替換了

只在你要裝飾的函式上面加上下面程式碼

1

2

3

4

5

6

7

8

9

10

11

12

@login

def america():

#login() #執行前加上驗證

print("----歐美專區----")

def japan():

print("----日韓專區----")

@login

def henan():

#login() #執行前加上驗證

print("----河南專區----")

效果是一樣的。

你開心的玩著老王教你的新姿勢 ,玩著玩著就手賤給你的“河南專區”版塊 加了個引數,然後,結果 出錯了。。。

你:老王,老王,怎麼傳個引數就不行了呢?

老王:那必然呀,你呼叫henan時,其實是相當於呼叫的login,你的henan第一次呼叫時henan = login(henan), login就返回了inner的記憶體地址,第2次使用者自己呼叫henan("3p"),實際上相當於呼叫的時inner,但你的inner定義時並沒有設定引數,但你給他傳了個引數,所以自然就報錯了呀

你:但是我的 版塊需要傳引數呀,你不讓我傳不行呀。。。

老王:沒說不讓你傳,稍做改動便可。

老王:你再試試就好了 。 

你: 果然好使,大神就是大神呀。 。。 不過,如果有多個引數呢?

老王:。。。。老弟,你不要什麼都讓我教你吧,非固定引數你沒學過麼? *args,**kwargs...

你:噢 。。。還能這麼搞?,nb,我再試試。

你身陷這種新玩法中無法自拔,竟沒注意到老王已經離開,你媳婦告訴你說為了不打擾你加班,今晚帶孩子去跟她姐妹住 ,你覺得媳婦真體貼,最終,你終於搞定了所有需求,完全遵循開放-封閉原則,最終程式碼如下 。

#_*_coding:utf-8_*_

user_status = False #使用者登入了就把這個改成True

def login(func): #把要執行的模組從這裡傳進來

def inner(*args,**kwargs):#再定義一層函式

_username = "alex" #假裝這是DB裡存的使用者資訊

_password = "abc!23" #假裝這是DB裡存的使用者資訊

global user_status

if user_status == False:

username = input("user:")

password = input("pasword:")

if username == _username and password == _password:

print("welcome login....")

user_status = True

else:

print("wrong username or password!")

if user_status == True:

func(*args,**kwargs) # 看這裡看這裡,只要驗證通過了,就呼叫相應功能

return inner #使用者呼叫login時,只會返回inner的記憶體地址,下次再呼叫時加上()才會執行inner函式

def home():

print("---首頁----")

@login

def america():

#login() #執行前加上驗證

print("----歐美專區----")

def japan():

print("----日韓專區----")

# @login

def henan(style):

'''

:param style: 喜歡看什麼型別的,就傳進來

:return:

'''

#login() #執行前加上驗證

print("----河南專區----")

home()

# america = login(america) #你在這裡相當於把america這個函式替換了

henan = login(henan)

# #那使用者呼叫時依然寫

america()

henan("3p")

好了,總算完成了。