【函數】0五、裝飾器由淺入深


原文http://www.cnblogs.com/feixuelove1009/p/5541632.htmlhtml

  裝飾器的功能在不少語言中都有,名字也不盡相同,其實它體現的是一種設計模式,強調的是開放封閉原則,更多的用於後期功能升級而不是編寫新的代碼。裝飾器不光能裝飾函數,也能裝飾其餘的對象,好比類,但一般,咱們以裝飾函數爲例子介紹其用法。要理解在Python中裝飾器的原理,須要一步一步來。本文儘可能描述得淺顯易懂,從最基礎的內容講起。python


裝飾器的做用:增長被裝飾器裝飾的函數的功能
shell


1、Python的函數相關基礎編程

  第一,必須強調的是python是從上往下順序執行的,並且碰到函數的定義代碼塊是不會當即執行它的,只有等到該函數被調用時,纔會執行其內部的代碼塊。            設計模式

In [2]: def foo():
   ...:     print("foo函數被運行了")
   ...:     
   # 若是就這麼樣,foo裏的語句是不會被執行的;程序只是簡單的將定義代碼塊讀入內存中

再看看,順序執行的例子:安全

def foo():
    print("我是上面的函數定義!")

def foo():
    print("我是下面的函數定義!")
 
 foo()

運行結果:
 
我是下面的函數定義

  可見,由於順序執行的緣由,下面的foo將上面的foo覆蓋了。所以,在Python中代碼的放置位置是有要求的,不能隨意擺放,函數體要放在被調用的語句以前。bash

  其次,咱們還要先搞清楚幾樣東西:函數名函數體返回值函數的內存地址函數名加括號函數名被看成參數函數名加括號被看成參數返回函數名返回函數名加括號。             
app

對於以下的函數:      運維

In [10]: def foo():
    ...:     print("讓咱們乾點啥!")
    ...:     return "ok"
    ...: 

In [12]: foo
Out[12]: <function __main__.foo>    
        
In [11]: foo()
讓咱們乾點啥!
Out[11]: 'ok'

函數名: fooide

函數體: 第1-3行

返回值: 字符串「ok」 若是不顯示給出return的對象,那麼默認返回None

函數的內存地址: 當函數體被讀進內存後的保存位置,它由標識符即函數名foo引用,也就是說foo指向的是函數體在內存內的保存位置。

函數名加括號:foo(),函數的調用方法,只有見到這個括號,程序會根據函數名從內存中找到函數體,而後執行它



再看下面這個例子:

In [15]: def outer(func):
    ...:     def inner():
    ...:         print('我是內層函數!')
    ...:     return inner
    ...: 

In [16]: def foo():
    ...:     print('我是原始函數')
    ...:     

In [18]: outer(foo)
Out[18]: <function __main__.outer.<locals>.inner>

In [19]: outer(foo)()
我是內層函數!

In [20]: outer(foo())
我是原始函數
Out[20]: <function __main__.outer.<locals>.inner>

In [21]: outer(foo())()
我是原始函數
我是內層函數!

  在python中,一切都是對象,函數也不例外。所以能夠將函數名,甚至函數名加括號進行調用的方式做爲另外一個函數的參數

  上面代碼中,outer和foo是兩個函數,outer(foo)表示將foo函數的函數名當作參數傳遞給outer函數並執行outer函數;outer(foo())表示將foo函數執行後的返回值當作參數傳遞給outer函數並執行outer函數,因爲foo函數沒有指定返回值,實際上它傳遞給了outer函數一個None。注意其中的差異,有沒有括號是關鍵!

  一樣,在outer函數內部,返回了一個inner,它是在outer函數內部定義的一個函數,注意,因爲inner後面沒有加括號,因此返回的是inner的函數體,實際上也就是inner這個名字,一個簡單的引用而已。那麼,若是outer函數返回的是inner()呢?如今你應該已經很清楚了,它會先執行inner函數的內容,而後返回個None給outer,outer再把這個None返回給調用它的對象。

   請記住,函數名、函數加括號能夠被當作參數傳遞,也能夠被當作返回值return,有沒有括號是兩個大相徑庭的意思!           


2、裝飾器的使用場景

  裝飾器一般用於在不改變原有函數代碼和功能的狀況下,爲其添加額外的功能。好比在原函數執行前先執行點什麼,在執行後執行點什麼。

  讓咱們經過一個例子來看看,裝飾器的使用場景和體現的設計模式。(抱歉的是我設計不出更好的場景,只能引用武大神的案例加以演繹)

  有一個大公司,下屬的基礎平臺部負責內部應用程序及API的開發,有上百個業務部門負責不一樣的業務,他們各自調用基礎平臺部提供的不一樣函數處理本身的業務,狀況以下:

 # 基礎平臺部門開發了上百個函數
 def f1():
     print("業務部門1數據接口......")
 
 def f2():
     print("業務部門2數據接口......")
 
 def f3():
     print("業務部門3數據接口......")
  .
  .
  .
  
 def f100():
     print("業務部門100數據接口......")
     
 # 各部門分別調用
 f1()
 f2()
 f3()
 f100()

  因爲公司在創業初期,基礎平臺部開發這些函數時,因爲各類緣由,好比時間,好比考慮不周等等,沒有爲函數調用進行安全認證。如今,平臺部主管決定彌補這個缺陷,因而:

第一回,主管叫來了一個運維工程師,工程師跑上跑下逐個部門進行通知,讓他們在代碼里加上認證功能,然而,當天他被開除了。

第二回:主管又叫來了一個運維工程師,工程師用shell寫了個複雜的腳本,勉強實現了功能。但他很快就回去接着作運維了,不會開發的運維不是好運維....

第三回:主管叫來了一個python自動化開發工程師,哥們是這麼幹的:    只對基礎平臺的代碼進行重構,讓N個業務部門無需作任何修改。這哥們很快也被開了,連運維也沒得作。

def f1():
    #加入認證程序代碼
    print("業務部門1數據接口......")

def f2():
    # 加入認證程序代碼
    print("業務部門2數據接口......")

def f3():
    # 加入認證程序代碼
    print("業務部門3數據接口......")
.
.
.
    
def f100():
    #加入認證程序代碼
    print("業務部門100數據接口......")
    
# 各部門分別調用
f1()
f2()
f3()
f100()

第四回:主管又換了個工程師,他是這麼幹的:定義個認證函數,原來其餘的函數調用它,代碼以下框。可是,主管依然不滿意,不過這一次他解釋了爲何。主管說:寫代碼要遵循開放封閉原則,雖然在這個原則主要是針對面向對象開發,可是也適用於函數式編程,簡單來講,它規定已經實現的功能代碼內部不容許被修改,但外部能夠被擴展

封閉:已實現的功能代碼塊;

開放:對擴展開放。

若是將開放封閉原則應用在上述需求中,那麼就不容許在函數 f1 、f二、f3......f100的內部進行代碼   修改。遺憾的是,工程師沒有漂亮的女友,因此很快也被開除了。

def login():
    print("認證成功!")
    
    
def f1():
    login()
    print("業務部門1數據接口......")
def f2():
    login()
    print("業務部門2數據接口......")
def f3():
    login()
    print("業務部門3數據接口......")
def f100():
    login()
    print("業務部門100數據接口......")

# 各部門分別調用
f1()
f2()
f3()
f100()

第五回:已經沒有時間讓主管找別人來幹這活了,他決定親自上陣,而且打算在函數執行後再增長個日誌功能。主管是這麼想的:不會裝飾器的主管不是好碼農!要不爲啥我能當主管,你只能被管呢?嘿嘿。他的代碼以下:

def outer(func):
    def inner():
        print("認證成功!")
        result = func()
        print("日誌添加成功")        
        return result    
    return inner
    
@outer
def f1():
    print("業務部門1數據接口......")
    
@outer
def f2():
    print("業務部門2數據接口......")
    
@outer
def f3():
    print("業務部門3數據接口......")
    
@outer
def f100():
    print("業務部門100數據接口......")
    
# 各部門分別調用
f1()
f2()
f3()
f100()

  對於上述代碼,也是僅需對基礎平臺的代碼進行拓展,就能夠實如今其餘部門調用函數 f1 f2 f3 f100 以前都進行認證操做,在操做結束後保存日誌,而且其餘業務部門無需他們本身的代碼作任何修改,調用方式也不用變。「主管」寫完代碼後,以爲獨樂了不如衆樂樂,打算顯擺一下,因而寫了篇博客將過程進行了詳細的說明。


3、裝飾器的內部原理

下面咱們以f1函數爲例進行說明:

In [23]: def outer(func):
    ...:     def inner():
    ...:         print('認證成功!')
    ...:         result = func()
    ...:         print('日誌添加成功')
    ...:         return result
    ...:     return inner
    ...: 

In [24]: @outer        # f1 = outer(f1) = inner
    ...: def f1():
    ...:     print('業務部門1數據接口...')
    ...:     

In [25]: f1()
認證成功!
業務部門1數據接口...
日誌添加成功

運用咱們在第一部分介紹的知識來分析一下上面這段代碼:

  1. 程序開始運行,從上往下編譯,讀到def outer(func):的時候,發現這是個「一等公民」->函數,因而把函數體加載到內存裏,而後過。

  2. 讀到@outer的時候,程序被@這個語法糖吸引住了,知道這是個裝飾器,按規矩要當即執行的,因而程序開始運行@後面那個名字outer所定義的函數。(相信沒有人會愚蠢的將@outer寫到別的位置,它只能放在被裝飾的函數的上方最近處,不要空行。)

  3. 程序返回到outer函數,開始執行裝飾器的語法規則,這部分規則是定死的,是python的「法律」,不要問爲何。

    規則是:被裝飾的函數的名字會被看成參數傳遞給裝飾函數。裝飾函數執行它本身內部的代碼後,會將它的返回值賦值給被裝飾的函數。         

這裏面須要注意的是:

  • @outer和@outer()有區別:沒有括號時,outer函數依然會被執行,這和傳統的用括號才能調用函數不一樣,須要特別注意!那麼有括號呢?那是裝飾器的高級用法了,之後會介紹。

  • 是f1這個函數名(而不是f1()這樣被調用後)當作參數傳遞給裝飾函數outer,也就是:func = f1,@outer等於outer(f1),實際上傳遞了f1的函數體,而不是執行f1後的返回值。

  • outer函數return的是inner這個函數名,而不是inner()這樣被調用後的返回值。

  若是你對第一部分函數的基礎知識有清晰的瞭解,那麼上面的內容你應該很容易理解。

  4. 程序開始執行outer函數內部的內容,一開始它又碰到了一個函數,很繞是吧?固然,你能夠在 inner函數先後安排點別的代碼,但它們不是重點,並且有點小麻煩,下面會解釋。inner函數定義塊被程序觀察到後不會馬上執行,而是讀入內存中(這是潛規則)。

  5. 再往下,碰到return inner,返回值是個函數名,而且這個函數名會被賦值給f1這個被裝飾的函數,也就是f1 = inner。根據前面的知識,咱們知道,此時f1函數被新的函數inner覆蓋了(其實是f1這個函數名更改爲指向inner這個函數名指向的函數體內存地址,f1再也不指向它原來的函數體的內存地址),再日後調用f1的時候將執行inner函數內的代碼,而不是先前的函數體。那麼先前的函數體去哪了?還記得咱們將f1當作參數傳遞給func這個形參麼?func這個變量保存了老的函數在內存中的地址,經過它就能夠執行 老的函數體,你能在inner函數裏看到result = func()這句代碼,它就是這麼幹的!

  6.接下來,尚未結束。當業務部門,依然經過f1()的方式調用f1函數時,執行的就再也不是老的f1函數的代碼,而是inner函數的代碼。在本例中,它首先會打印個「認證成功」的提示,很顯然你能夠換成任意的代碼,這只是個示例;而後,它會執行func函數並將返回值賦值個變量result,這個func函數就是老的f1函數;接着,它又打印了「日誌保存」的提示,這也只是個示例,能夠換成任何你想要的;最後返回result這個變量。咱們在業務部門的代碼上能夠用 r = f1()的方式接受result的值。

  7.以上流程走完後,你應該看出來了,在沒有對業務部門的代碼和接口調用方式作任何修改的同時,也沒有對基礎平臺部原有的代碼作內部修改,僅僅是添加了一個裝飾函數,就實現了咱們的需求,在函數調用前先認證,調用後寫入日誌。這就是裝飾器的最大做用。


問題:那麼爲何咱們要搞一個outer函數一個inner函數這麼複雜呢?一層函數不行嗎?  

答:請注意,@outer這句代碼在程序執行到這裏的時候就會自動執行outer函數內部的代碼,若是不封裝一下,在業務部門還未進行調用的時候,就執行了些什麼,這和初衷有點不符。固然,若是你對這個有需求也不是不行。請看下面的例子,它只有一層函數。  

In [33]: def outer(func):
    ...:     print('認證成功!')
    ...:     result = func()
    ...:     print('日誌添加成功')
    ...:     return result
    ...: 

In [34]: @outer
    ...: def f1():
    ...:     print('業務部門1數據接口...')
    ...:     
認證成功!
業務部門1數據接口...
日誌添加成功

# 業務部門並無執行f1函數

  看到沒?我只是定義好了函數,業務部門尚未調用f1函數呢,程序就把工做全作了。這就是封裝一層函數的緣由。


3、裝飾器的參數傳遞

一、被裝飾函數的參數傳遞

  細心的朋友可能已經發現了,上面的例子中,f1函數沒有參數,在實際狀況中確定會須要參數的,那參數怎麼傳遞的呢?

一個參數的狀況:

In [13]: def outer(func):
    ...:     def inner(username):
    ...:         print('認證成功')
    ...:         result = func(username)
    ...:         print('日誌添加成功')
    ...:         return result
    ...:     return inner
    ...: 

In [14]: @outer
    ...: def f1(name):
    ...:     print("{}正在鏈接業務部門1數據接口。。。".format(name))
    ...:     

In [15]: f1("jack")
認證成功
jack正在鏈接業務部門1數據接口。。。
日誌添加成功

  在inner函數的定義部分也加上一個參數,調用func函數的時候傳遞這個參數,很好理解吧?可問題又來了,那麼另一個部門調用的f2有2個參數呢?f3有3個參數呢?你怎麼傳遞?

很簡單,咱們有*args和**kwargs嘛!號稱「萬能參數」!

簡單修改一下上面的代碼:

In [17]: def outer(func):
    ...:     def inner(*args, **kwargs):
    ...:         print('認證成功')
    ...:         result = func(*args, **kwargs)
    ...:         print('日誌添加成功')
    ...:         return result
    ...:     return inner
    ...: 

In [18]: @outer
    ...: def f1(name, age):
    ...:     print("{}正在鏈接業務部門1數據接口。。。".format(name))
    ...:     

In [19]: f1("jack", 18)
認證成功
jack正在鏈接業務部門1數據接口。。。
日誌添加成功


二、裝飾器本身的參數

  裝飾器本身的參數?裝飾器本身的參數不是被裝飾的函數嗎?

     那裝飾器如何傳入本身的參數呢?

In [31]: # 認證函數

In [32]: def auth(request, kwargs):
    ...:     print("認證成功!")
    ...:     

In [33]: # 日誌函數

In [34]: def log(request, kwargs):
    ...:     print("日誌添加成功!")
    ...:     

In [35]: # 裝飾器函數,接收2個參數,這兩個參數應該是某個函數的名字。

In [41]: def Filter(auth_func, log_func):
    ...:     # 第一層封裝,auth和log函數的參數值被傳遞到了這裏
    ...:     def outer(main_func):
    ...:         # 第二層封裝,f1函數實際上被傳遞給了main_func這個參數
    ...:         def wrapper(request, kargs): 
    ...:         # 下面代碼的判斷邏輯不重要,重要的是參數的引用和返回值
    ...:             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
    ...: 
    # 注意了,這裏的裝飾器函數有參數哦,它的意思是先執行Filter函數
    # 而後將Filter函數的返回值做爲裝飾器函數的名字返回到這裏,因此,
    # 其實這裏等價於,Filter(auth,log) = outer , @Filter(auth,log) =  @outer
        #  也等價於f1=Filter(auth, log)(f1)=@outer=wrapper
    
In [44]: @Filter(auth, log)  
    ...: def f1(name, age):
    ...:     print("{} 正在鏈接業務部門1數據接口。。。".format(name))
    ...:     

In [45]: f1("jack", 18)
認證成功!
jack 正在鏈接業務部門1數據接口。。。
日誌添加成功!

    又繞暈了?其實你能夠這麼理解,先執行Filter函數,得到它的返回值outer,再執行@outer裝飾器語法。


一個函數返回一個不帶參數的裝飾器就是帶參數的裝飾器


由於裝飾器自身的參數不能直接傳入因此在裝飾器在裝飾器外再封裝一層函數的目地就是爲了給裝飾器傳參,


4、更進一步的思考

一個函數能夠被多個函數裝飾嗎?能夠的!看下面的例子!

In [25]: def outer1(func):  
    ...:     def inner(*args, **kwargs):
    ...:         print('認證成功')          
    ...:         result = func(*args, **kwargs)
    ...:         print('日誌添加成功')      
    ...:         return result
    ...:     return inner
    ...: 
In [25]: def outer1(func):
    ...:     def inner(*args, **kwargs):
    ...:         print('認證成功')
    ...:         result = func(*args, **kwargs)
    ...:         print('日誌添加成功')
    ...:         return result
    ...:     return inner
    ...: 

In [26]: def outer2(func):
    ...:     def inner(*args, **kwargs):
    ...:         print('一條歡迎信息。。。')
    ...:         result = func(*args, **kwargs)
    ...:         print('一條歡送信息。。。')
    ...:         return result
    ...:     return inner
    ...: 

In [27]: @outer1
    ...: @outer2
    ...: def f1(name, age):
    ...:     print("{} 正在鏈接業務部門1數據接口。。.".format(name))
    ...:     

In [28]: f1("jack", 18)
認證成功
一條歡迎信息。。。
jack 正在鏈接業務部門1數據接口。。.
一條歡送信息。。。
日誌添加成功
相關文章
相關標籤/搜索