程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> Python >> Python裝飾器由淺入深

Python裝飾器由淺入深

編輯:Python

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

  (注:以下使用Python3.5.1環境)

一、Python的函數相關基礎

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

 def foo():
     print("foo函數被運行了!")
     
 
 如果就這麼樣,foo裡的語句是不會被執行的。
 程序只是簡單的將定義代碼塊讀入內存中。

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

 def foo():
     print("我是上面的函數定義!")
 
 def foo():
     print("我是下面的函數定義!")
 
 foo()
 
 運行結果:
 
 我是下面的函數定義

  可見,因為順序執行的原因,下面的foo將上面的foo覆蓋了。因此,在Python中代碼的放置位置是有要求的,不能隨意擺放,函數體要放在被調用的語句之前。

  其次,我們還要先搞清楚幾樣東西:函數名、函數體、返回值,函數的內存地址、函數名加括號、函數名被當作參數、函數名加括號被當作參數、返回函數名、返回函數名加括號。對於如下的函數:

 def foo():
     print("讓我們干點啥!")
     return "ok"
 
 foo()

  函數名:        foo

  函數體:        第1-3行

  返回值:        字符串“ok”    如果不顯式給出return的對象,那麼默認返回None

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

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

  再看下面這個例子:

 def outer(func):
     def inner():
         print("我是內層函數!")
     return inner
 
 def foo():
     print("我是原始函數!")
     
 outer(foo)
10 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,有沒有括號是兩個截然不同的意思!


二、裝飾器的使用場景

  裝飾器通常用於在不改變原有函數代碼和功能的情況下,為其添加額外的功能。比如在原函數執行前先執行點什麼,在執行後執行點什麼。

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

  有一個大公司,下屬的基礎平台部負責內部應用程序及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 、f2、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()

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

#/usr/bin/env python
#coding:utf-8


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 之前都進行認證操作,在操作結束後保存日志,並且其他業務部門無需他們自己的代碼做任何修改,調用方式也不用變。“主管”寫完代碼後,覺得獨樂了不如眾樂樂,打算顯擺一下,於是寫了篇博客將過程進行了詳細的說明。


 三、裝飾器的內部原理

  下面我們以f1函數為例進行說明:

def outer(func):
    def inner():
        print("認證成功!")
        result = func()
        print("日志添加成功")
        return result
    return inner

@outer
def f1():
    print("業務部門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函數內部的代碼,如果不封裝一下,在業務部門還未進行調用的時候,就執行了些什麼,這和初衷有點不符。當然,如果你對這個有需求也不是不行。請看下面的例子,它只有一層函數。

def outer(func):
        print("認證成功!")
        result = func()
        print("日志添加成功")
        return result

@outer
def f1():

    print("業務部門1數據接口......")

# 業務部門並沒有開始執行f1函數


執行結果:

認證成功!
業務部門1數據接口......
日志添加成功

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

 


 

三、裝飾器的參數傳遞

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

  一個參數的情況:

def outer(func):
    def inner(username):
        print("認證成功!")
        result = func(username)
        print("日志添加成功")
        return result
    return inner

@outer
def f1(name):

    print("%s 正在連接業務部門1數據接口......"%name)

# 調用方法
f1("jack")

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

  很簡單,我們有*args和**kwargs嘛!號稱“萬能參數”!簡單修改一下上面的代碼:

def outer(func):
    def inner(*args,**kwargs):
        print("認證成功!")
        result = func(*args,**kwargs)
        print("日志添加成功")
        return result
    return inner

@outer
def f1(name,age):

    print("%s 正在連接業務部門1數據接口......"%name)

# 調用方法
f1("jack",18)

四、更進一步的思考

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

def outer1(func):
    def inner(*args,**kwargs):
        print("認證成功!")
        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 正在連接業務部門1數據接口......"%name)

# 調用方法
f1("jack",18)


執行結果:

認證成功!
一條歡迎信息。。。
jack 正在連接業務部門1數據接口......
一條歡送信息。。。
日志添加成功

  更進一步的,裝飾器自己可以有參數嗎?可以的!看下面的例子:

# 認證函數
def  auth(request,kargs):
    print("認證成功!")
# 日志函數
def log(request,kargs):
    print("日志添加成功")
# 裝飾器函數。接收兩個參數,這兩個參數應該是某個函數的名字。
def Filter(auth_func,log_func):
    # 第一層封裝,f1函數實際上被傳遞給了main_fuc這個參數
    def outer(main_func):
        # 第二層封裝,auth和log函數的參數值被傳遞到了這裡
        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
@Filter(auth,log)
def f1(name,age):

    print("%s 正在連接業務部門1數據接口......"%name)

# 調用方法
f1("jack",18)





運行結果:

認證成功!
jack 正在連接業務部門1數據接口......
日志添加成功

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


 

   看到這,是不是覺得自己已經天下無敵了,有種裝飾器盡在我手的感覺?接下來請看續集“論python裝飾器的高級使用”.......

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved