python装饰器由浅入深_详解Python装饰器由浅入深
裝飾器的功能在很多語(yǔ)言中都有,名字也不盡相同,其實(shí)它體現(xiàn)的是一種設(shè)計(jì)模式,強(qiáng)調(diào)的是開(kāi)放封閉原則,更多的用于后期功能升級(jí)而不是編寫(xiě)新的代碼。裝飾器不光能裝飾函數(shù),也能裝飾其他的對(duì)象,比如類(lèi),但通常,我們以裝飾函數(shù)為例子介紹其用法。要理解在Python中裝飾器的原理,需要一步一步來(lái)。本文盡量描述得淺顯易懂,從最基礎(chǔ)的內(nèi)容講起。
(注:以下使用Python3.5.1環(huán)境)
一、Python的函數(shù)相關(guān)基礎(chǔ)
第一,必須強(qiáng)調(diào)的是python是從上往下順序執(zhí)行的,而且碰到函數(shù)的定義代碼塊是不會(huì)立即執(zhí)行它的,只有等到該函數(shù)被調(diào)用時(shí),才會(huì)執(zhí)行其內(nèi)部的代碼塊。def foo():
print("foo函數(shù)被運(yùn)行了!")
如果就這么樣,foo里的語(yǔ)句是不會(huì)被執(zhí)行的。
程序只是簡(jiǎn)單的將定義代碼塊讀入內(nèi)存中。
再看看,順序執(zhí)行的例子:def foo():
print("我是上面的函數(shù)定義!")
def foo():
print("我是下面的函數(shù)定義!")
foo()
運(yùn)行結(jié)果:
我是下面的函數(shù)定義
可見(jiàn),因?yàn)轫樞驁?zhí)行的原因,下面的foo將上面的foo覆蓋了。因此,在Python中代碼的放置位置是有要求的,不能隨意擺放,函數(shù)體要放在被調(diào)用的語(yǔ)句之前。
其次,我們還要先搞清楚幾樣?xùn)|西:函數(shù)名、函數(shù)體、返回值,函數(shù)的內(nèi)存地址、函數(shù)名加括號(hào)、函數(shù)名被當(dāng)作參數(shù)、函數(shù)名加括號(hào)被當(dāng)作參數(shù)、返回函數(shù)名、返回函數(shù)名加括號(hào)。對(duì)于如下的函數(shù):def foo():
print("讓我們干點(diǎn)啥!")
return "ok"
foo()
函數(shù)名: foo
函數(shù)體: 第1-3行
返回值: 字符串“ok” 如果不顯式給出return的對(duì)象,那么默認(rèn)返回None
函數(shù)的內(nèi)存地址: 當(dāng)函數(shù)體被讀進(jìn)內(nèi)存后的保存位置,它由標(biāo)識(shí)符即函數(shù)名foo引用,
也就是說(shuō)foo指向的是函數(shù)體在內(nèi)存內(nèi)的保存位置。
函數(shù)名加括號(hào): 例如foo(),函數(shù)的調(diào)用方法,只有見(jiàn)到這個(gè)括號(hào),程序會(huì)根據(jù)
函數(shù)名從內(nèi)存中找到函數(shù)體,然后執(zhí)行它
再看下面這個(gè)例子:def outer(func):
def inner():
print("我是內(nèi)層函數(shù)!")
return inner
def foo():
print("我是原始函數(shù)!")
outer(foo)
outer(foo())
在python中,一切都是對(duì)象,函數(shù)也不例外。因此可以將函數(shù)名,甚至函數(shù)名加括號(hào)進(jìn)行調(diào)用的方式作為另一個(gè)函數(shù)的返回值。上面代碼中,outer和foo是兩個(gè)函數(shù),outer(foo)表示將foo函數(shù)的函數(shù)名當(dāng)做參數(shù)傳遞給outer函數(shù)并執(zhí)行outer函數(shù);outer(foo())表示將foo函數(shù)執(zhí)行后的返回值當(dāng)做參數(shù)傳遞給outer函數(shù)并執(zhí)行outer函數(shù),由于foo函數(shù)沒(méi)有指定返回值,實(shí)際上它傳遞給了outer函數(shù)一個(gè)None。注意其中的差別,有沒(méi)有括號(hào)是關(guān)鍵!
同樣,在outer函數(shù)內(nèi)部,返回了一個(gè)inner,它是在outer函數(shù)內(nèi)部定義的一個(gè)函數(shù),注意,由于inner后面沒(méi)有加括號(hào),所以返回的是inner的函數(shù)體,實(shí)際上也就是inner這個(gè)名字,一個(gè)簡(jiǎn)單的引用而已。那么,如果outer函數(shù)返回的是inner()呢?現(xiàn)在你應(yīng)該已經(jīng)很清楚了,它會(huì)先執(zhí)行inner函數(shù)的內(nèi)容,然后返回個(gè)None給outer,outer再把這個(gè)None返回給調(diào)用它的對(duì)象。
請(qǐng)記住,函數(shù)名、函數(shù)加括號(hào)可以被當(dāng)做參數(shù)傳遞,也可以被當(dāng)做返回值return,有沒(méi)有括號(hào)是兩個(gè)截然不同的意思!
二、裝飾器的使用場(chǎng)景
裝飾器通常用于在不改變?cè)泻瘮?shù)代碼和功能的情況下,為其添加額外的功能。比如在原函數(shù)執(zhí)行前先執(zhí)行點(diǎn)什么,在執(zhí)行后執(zhí)行點(diǎn)什么。
讓我們通過(guò)一個(gè)例子來(lái)看看,裝飾器的使用場(chǎng)景和體現(xiàn)的設(shè)計(jì)模式。(抱歉的是我設(shè)計(jì)不出更好的場(chǎng)景,只能引用武大神的案例加以演繹)
有一個(gè)大公司,下屬的基礎(chǔ)平臺(tái)部負(fù)責(zé)內(nèi)部應(yīng)用程序及API的開(kāi)發(fā),有上百個(gè)業(yè)務(wù)部門(mén)負(fù)責(zé)不同的業(yè)務(wù),他們各自調(diào)用基礎(chǔ)平臺(tái)部提供的不同函數(shù)處理自己的業(yè)務(wù),情況如下:# 基礎(chǔ)平臺(tái)部門(mén)開(kāi)發(fā)了上百個(gè)函數(shù)
def f1():
print("業(yè)務(wù)部門(mén)1數(shù)據(jù)接口......")
def f2():
print("業(yè)務(wù)部門(mén)2數(shù)據(jù)接口......")
def f3():
print("業(yè)務(wù)部門(mén)3數(shù)據(jù)接口......")
def f100():
print("業(yè)務(wù)部門(mén)100數(shù)據(jù)接口......")
#各部門(mén)分別調(diào)用
f1()
f2()
f3()
f100()
由于公司在創(chuàng)業(yè)初期,基礎(chǔ)平臺(tái)部開(kāi)發(fā)這些函數(shù)時(shí),由于各種原因,比如時(shí)間,比如考慮不周等等,沒(méi)有為函數(shù)調(diào)用進(jìn)行安全認(rèn)證。現(xiàn)在,平臺(tái)部主管決定彌補(bǔ)這個(gè)缺陷,于是:
第一回,主管叫來(lái)了一個(gè)運(yùn)維工程師,工程師跑上跑下逐個(gè)部門(mén)進(jìn)行通知,讓他們?cè)诖a里加上認(rèn)證功能,然而,當(dāng)天他被開(kāi)除了。
第二回:主管又叫來(lái)了一個(gè)運(yùn)維工程師,工程師用shell寫(xiě)了個(gè)復(fù)雜的腳本,勉強(qiáng)實(shí)現(xiàn)了功能。但他很快就回去接著做運(yùn)維了,不會(huì)開(kāi)發(fā)的運(yùn)維不是好運(yùn)維....
第三回:主管叫來(lái)了一個(gè)python自動(dòng)化開(kāi)發(fā)工程師,哥們是這么干的:只對(duì)基礎(chǔ)平臺(tái)的代碼進(jìn)行重構(gòu),讓N個(gè)業(yè)務(wù)部門(mén)無(wú)需做任何修改。這哥們很快也被開(kāi)了,連運(yùn)維也沒(méi)得做。def f1():
#加入認(rèn)證程序代碼
print("業(yè)務(wù)部門(mén)1數(shù)據(jù)接口......")
def f2():
# 加入認(rèn)證程序代碼
print("業(yè)務(wù)部門(mén)2數(shù)據(jù)接口......")
def f3():
# 加入認(rèn)證程序代碼
print("業(yè)務(wù)部門(mén)3數(shù)據(jù)接口......")
def f100():
#加入認(rèn)證程序代碼
print("業(yè)務(wù)部門(mén)100數(shù)據(jù)接口......")
#各部門(mén)分別調(diào)用
f1()
f2()
f3()
f100()
第四回:主管又換了個(gè) 工程師,他是這么干的:定義個(gè)認(rèn)證函數(shù),原來(lái)其他的函數(shù)調(diào)用它,代碼如下框。但是,主管依然不滿意,不過(guò)這一次他解釋了為什么。主管說(shuō):寫(xiě)代碼要遵循開(kāi)放封閉原則,雖然在這個(gè)原則主要是針對(duì)面向?qū)ο箝_(kāi)發(fā),但是也適用于函數(shù)式編程,簡(jiǎn)單來(lái)說(shuō),它規(guī)定已經(jīng)實(shí)現(xiàn)的功能代碼內(nèi)部不允許被修改,但外部可以被擴(kuò)展,即:封閉:已實(shí)現(xiàn)的功能代碼塊;開(kāi)放:對(duì)擴(kuò)展開(kāi)放。如果將開(kāi)放封閉原則應(yīng)用在上述需求中,那么就不允許在函數(shù) f1 、f2、f3......f100的內(nèi)部進(jìn)行代碼修改。遺憾的是,工程師沒(méi)有漂亮的女朋友,所以很快也被開(kāi)除了。def login():
print("認(rèn)證成功!")
def f1():
login()
print("業(yè)務(wù)部門(mén)1數(shù)據(jù)接口......")
def f2():
login()
print("業(yè)務(wù)部門(mén)2數(shù)據(jù)接口......")
def f3():
login()
print("業(yè)務(wù)部門(mén)3數(shù)據(jù)接口......")
def f100():
login()
print("業(yè)務(wù)部門(mén)100數(shù)據(jù)接口......")
#各部門(mén)分別調(diào)用
f1()
f2()
f3()
f100()
第五回:已經(jīng)沒(méi)有時(shí)間讓主管找別人來(lái)干這活了,他決定親自上陣,并且打算在函數(shù)執(zhí)行后再增加個(gè)日志功能。主管是這么想的:不會(huì)裝飾器的主管不是好碼農(nóng)!要不為啥我能當(dāng)主管,你只能被管呢?嘿嘿。他的代碼如下:#/usr/bin/env python
#coding:utf-8
def outer(func):
def inner():
print("認(rèn)證成功!")
result = func()
print("日志添加成功")
return result
return inner
@outer
def f1():
print("業(yè)務(wù)部門(mén)1數(shù)據(jù)接口......")
@outer
def f2():
print("業(yè)務(wù)部門(mén)2數(shù)據(jù)接口......")
@outer
def f3():
print("業(yè)務(wù)部門(mén)3數(shù)據(jù)接口......")
@outer
def f100():
print("業(yè)務(wù)部門(mén)100數(shù)據(jù)接口......")
#各部門(mén)分別調(diào)用
f1()
f2()
f3()
f100()
對(duì)于上述代碼,也是僅需對(duì)基礎(chǔ)平臺(tái)的代碼進(jìn)行拓展,就可以實(shí)現(xiàn)在其他部門(mén)調(diào)用函數(shù) f1 f2 f3 f100 之前都進(jìn)行認(rèn)證操作,在操作結(jié)束后保存日志,并且其他業(yè)務(wù)部門(mén)無(wú)需他們自己的代碼做任何修改,調(diào)用方式也不用變。“主管”寫(xiě)完代碼后,覺(jué)得獨(dú)樂(lè)了不如眾樂(lè)樂(lè),打算顯擺一下,于是寫(xiě)了篇博客將過(guò)程進(jìn)行了詳細(xì)的說(shuō)明。
三、裝飾器的內(nèi)部原理、
下面我們以f1函數(shù)為例進(jìn)行說(shuō)明:def outer(func):
def inner():
print("認(rèn)證成功!")
result = func()
print("日志添加成功")
return result
return inner
@outer
def f1():
print("業(yè)務(wù)部門(mén)1數(shù)據(jù)接口......")
運(yùn)用我們?cè)诘谝徊糠纸榻B的知識(shí)來(lái)分析一下上面這段代碼:
程序開(kāi)始運(yùn)行,從上往下編譯,讀到def outer(func):的時(shí)候,發(fā)現(xiàn)這是個(gè)“一等公民”->函數(shù),于是把函數(shù)體加載到內(nèi)存里,然后過(guò)。
讀到@outer的時(shí)候,程序被@這個(gè)語(yǔ)法糖吸引住了,知道這是個(gè)裝飾器,按規(guī)矩要立即執(zhí)行的,于是程序開(kāi)始運(yùn)行@后面那個(gè)名字outer所定義的函數(shù)。(相信沒(méi)有人會(huì)愚蠢的將@outer寫(xiě)到別的位置,它只能放在被裝飾的函數(shù)的上方最近處,不要空行。)
程序返回到outer函數(shù),開(kāi)始執(zhí)行裝飾器的語(yǔ)法規(guī)則,這部分規(guī)則是定死的,是python的“法律”,不要問(wèn)為什么。規(guī)則是:被裝飾的函數(shù)的名字會(huì)被當(dāng)作參數(shù)傳遞給裝飾函數(shù)。裝飾函數(shù)執(zhí)行它自己內(nèi)部的代碼后,會(huì)將它的返回值賦值給被裝飾的函數(shù)。
如下圖所示:
這里面需要注意的是:
@outer和@outer()有區(qū)別,沒(méi)有括號(hào)時(shí),outer函數(shù)依然會(huì)被執(zhí)行,這和傳統(tǒng)的用括號(hào)才能調(diào)用函數(shù)不同,需要特別注意!那么有括號(hào)呢?那是裝飾器的高級(jí)用法了,以后會(huì)介紹。
是f1這個(gè)函數(shù)名(而不是f1()這樣被調(diào)用后)當(dāng)做參數(shù)傳遞給裝飾函數(shù)outer,也就是:func = f1,@outer等于outer(f1),實(shí)際上傳遞了f1的函數(shù)體,而不是執(zhí)行f1后的返回值。
outer函數(shù)return的是inner這個(gè)函數(shù)名,而不是inner()這樣被調(diào)用后的返回值。
如果你對(duì)第一部分函數(shù)的基礎(chǔ)知識(shí)有清晰的了解,那么上面的內(nèi)容你應(yīng)該很容易理解。
4. 程序開(kāi)始執(zhí)行outer函數(shù)內(nèi)部的內(nèi)容,一開(kāi)始它又碰到了一個(gè)函數(shù),很繞是吧?當(dāng)然,你可以在 inner函數(shù)前后安排點(diǎn)別的代碼,但它們不是重點(diǎn),而且有點(diǎn)小麻煩,下面會(huì)解釋。inner函數(shù)定義塊被程序觀察到后不會(huì)立刻執(zhí)行,而是讀入內(nèi)存中(這是潛規(guī)則)。
5. 再往下,碰到return inner,返回值是個(gè)函數(shù)名,并且這個(gè)函數(shù)名會(huì)被賦值給f1這個(gè)被裝飾的函數(shù),也就是f1 = inner。根據(jù)前面的知識(shí),我們知道,此時(shí)f1函數(shù)被新的函數(shù)inner覆蓋了(實(shí)際上是f1這個(gè)函數(shù)名更改成指向inner這個(gè)函數(shù)名指向的函數(shù)體內(nèi)存地址,f1不再指向它原來(lái)的函數(shù)體的內(nèi)存地址),再往后調(diào)用f1的時(shí)候?qū)?zhí)行inner函數(shù)內(nèi)的代碼,而不是先前的函數(shù)體。那么先前的函數(shù)體去哪了?還記得我們將f1當(dāng)做參數(shù)傳遞給func這個(gè)形參么?func這個(gè)變量保存了老的函數(shù)在內(nèi)存中的地址,通過(guò)它就可以執(zhí)行 老的函數(shù)體,你能在inner函數(shù)里看到result = func()這句代碼,它就是這么干的!
6.接下來(lái),還沒(méi)有結(jié)束。當(dāng)業(yè)務(wù)部門(mén),依然通過(guò)f1()的方式調(diào)用f1函數(shù)時(shí),執(zhí)行的就不再是老的f1函數(shù)的代碼,而是inner函數(shù)的代碼。在本例中,它首先會(huì)打印個(gè)“認(rèn)證成功”的提示,很顯然你可以換成任意的代碼,這只是個(gè)示例;然后,它會(huì)執(zhí)行func函數(shù)并將返回值賦值個(gè)變量result,這個(gè)func函數(shù)就是老的f1函數(shù);接著,它又打印了“日志保存”的提示,這也只是個(gè)示例,可以換成任何你想要的;最后返回result這個(gè)變量。我們?cè)跇I(yè)務(wù)部門(mén)的代碼上可以用 r = f1()的方式接受result的值。
7.以上流程走完后,你應(yīng)該看出來(lái)了,在沒(méi)有對(duì)業(yè)務(wù)部門(mén)的代碼和接口調(diào)用方式做任何修改的同時(shí),也沒(méi)有對(duì)基礎(chǔ)平臺(tái)部原有的代碼做內(nèi)部修改,僅僅是添加了一個(gè)裝飾函數(shù),就實(shí)現(xiàn)了我們的需求,在函數(shù)調(diào)用前先認(rèn)證,調(diào)用后寫(xiě)入日志。這就是裝飾器的最大作用。
問(wèn)題:那么為什么我們要搞一個(gè)outer函數(shù)一個(gè)inner函數(shù)這么復(fù)雜呢?一層函數(shù)不行嗎?
答:請(qǐng)注意,@outer這句代碼在程序執(zhí)行到這里的時(shí)候就會(huì)自動(dòng)執(zhí)行outer函數(shù)內(nèi)部的代碼,如果不封裝一下,在業(yè)務(wù)部門(mén)還未進(jìn)行調(diào)用的時(shí)候,就執(zhí)行了些什么,這和初衷有點(diǎn)不符。當(dāng)然,如果你對(duì)這個(gè)有需求也不是不行。請(qǐng)看下面的例子,它只有一層函數(shù)。def outer(func):
print("認(rèn)證成功!")
result = func()
print("日志添加成功")
return result
@outer
def f1():
print("業(yè)務(wù)部門(mén)1數(shù)據(jù)接口......")
# 業(yè)務(wù)部門(mén)并沒(méi)有開(kāi)始執(zhí)行f1函數(shù)
執(zhí)行結(jié)果:
認(rèn)證成功!
業(yè)務(wù)部門(mén)1數(shù)據(jù)接口......
日志添加成功
看到?jīng)]?我只是定義好了函數(shù),業(yè)務(wù)部門(mén)還沒(méi)有調(diào)用f1函數(shù)呢,程序就把工作全做了。這就是封裝一層函數(shù)的原因。
四、裝飾器的參數(shù)傳遞
細(xì)心的朋友可能已經(jīng)發(fā)現(xiàn)了,上面的例子中,f1函數(shù)沒(méi)有參數(shù),在實(shí)際情況中肯定會(huì)需要參數(shù)的,那參數(shù)怎么傳遞的呢?
一個(gè)參數(shù)的情況:def outer(func):
def inner(username):
print("認(rèn)證成功!")
result = func(username)
print("日志添加成功")
return result
return inner
@outer
def f1(name):
print("%s 正在連接業(yè)務(wù)部門(mén)1數(shù)據(jù)接口......"%name)
# 調(diào)用方法
f1("jack")
在inner函數(shù)的定義部分也加上一個(gè)參數(shù),調(diào)用func函數(shù)的時(shí)候傳遞這個(gè)參數(shù),很好理解吧?可問(wèn)題又來(lái)了,那么另外一個(gè)部門(mén)調(diào)用的f2有2個(gè)參數(shù)呢?f3有3個(gè)參數(shù)呢?你怎么傳遞?
很簡(jiǎn)單,我們有*args和**kwargs嘛!號(hào)稱(chēng)“萬(wàn)能參數(shù)”!簡(jiǎn)單修改一下上面的代碼:def outer(func):
def inner(*args,**kwargs):
print("認(rèn)證成功!")
result = func(*args,**kwargs)
print("日志添加成功")
return result
return inner
@outer
def f1(name,age):
print("%s 正在連接業(yè)務(wù)部門(mén)1數(shù)據(jù)接口......"%name)
# 調(diào)用方法
f1("jack",18)
五、更進(jìn)一步的思考
一個(gè)函數(shù)可以被多個(gè)函數(shù)裝飾嗎?可以的!看下面的例子!def outer1(func):
def inner(*args,**kwargs):
print("認(rèn)證成功!")
result = func(*args,**kwargs)
print("日志添加成功")
return result
return inner
def outer2(func):
def inner(*args,**kwargs):
print("一條歡迎信息。。。")
result = func(*args,**kwargs)
print("一條歡送信息。。。")
return result
return inner
@outer1
@outer2
def f1(name,age):
print("%s 正在連接業(yè)務(wù)部門(mén)1數(shù)據(jù)接口......"%name)
# 調(diào)用方法
f1("jack",18)
執(zhí)行結(jié)果:
認(rèn)證成功!
一條歡迎信息。。。
jack 正在連接業(yè)務(wù)部門(mén)1數(shù)據(jù)接口......
一條歡送信息。。。
日志添加成功
更進(jìn)一步的,裝飾器自己可以有參數(shù)嗎?可以的!看下面的例子:# 認(rèn)證函數(shù)
def auth(request,kargs):
print("認(rèn)證成功!")
# 日志函數(shù)
def log(request,kargs):
print("日志添加成功")
# 裝飾器函數(shù)。接收兩個(gè)參數(shù),這兩個(gè)參數(shù)應(yīng)該是某個(gè)函數(shù)的名字。
def Filter(auth_func,log_func):
# 第一層封裝,f1函數(shù)實(shí)際上被傳遞給了main_fuc這個(gè)參數(shù)
def outer(main_func):
# 第二層封裝,auth和log函數(shù)的參數(shù)值被傳遞到了這里
def wrapper(request,kargs):
# 下面代碼的判斷邏輯不重要,重要的是參數(shù)的引用和返回值
before_result = auth(request,kargs)
if(before_result != None):
return before_result;
main_result = main_func(request,kargs)
if(main_result != None):
return main_result;
after_result = log(request,kargs)
if(after_result != None):
return after_result;
return wrapper
return outer
# 注意了,這里的裝飾器函數(shù)有參數(shù)哦,它的意思是先執(zhí)行filter函數(shù)
# 然后將filter函數(shù)的返回值作為裝飾器函數(shù)的名字返回到這里,所以,
# 其實(shí)這里,Filter(auth,log) = outer , @Filter(auth,log) = @outer
@Filter(auth,log)
def f1(name,age):
print("%s 正在連接業(yè)務(wù)部門(mén)1數(shù)據(jù)接口......"%name)
# 調(diào)用方法
f1("jack",18)
運(yùn)行結(jié)果:
認(rèn)證成功!
jack 正在連接業(yè)務(wù)部門(mén)1數(shù)據(jù)接口......
日志添加成功
又繞暈了?其實(shí)你可以這么理解,先執(zhí)行Filter函數(shù),獲得它的返回值outer,再執(zhí)行@outer裝飾器語(yǔ)法。
以上就是本文的全部?jī)?nèi)容,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來(lái)一定的幫助,同時(shí)也希望多多支持PHP中文網(wǎng)!
更多詳解Python裝飾器由淺入深相關(guān)文章請(qǐng)關(guān)注PHP中文網(wǎng)!
本文原創(chuàng)發(fā)布php中文網(wǎng),轉(zhuǎn)載請(qǐng)注明出處,感謝您的尊重!
總結(jié)
以上是生活随笔為你收集整理的python装饰器由浅入深_详解Python装饰器由浅入深的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: python 表示图论_Python 图
- 下一篇: .so文件反编译_java加密防止反编译