python 裝飾器詳解

【轉】做爲許多語言都存在的高級語法之一,裝飾器是你必須掌握的知識點。python

裝飾器(Decorator):從字面上理解,就是裝飾對象的器件。能夠在不修改原有代碼的狀況下,爲被裝飾的對象增長新的功能或者附加限制條件或者幫助輸出。shell

  • 裝飾器有不少種,有函數的裝飾器,也有類的裝飾器。裝飾器在不少語言中的名字也不盡相同,它體現的是設計模式中的裝飾模式,強調的是開放封閉原則。
  • 裝飾器的語法是將  @ 裝飾器名,放在被裝飾對象上面 。
    # 語法
    @dec def func(): pass

在進行裝飾器的介紹以前,咱們必須先明確幾個概念和原則:設計模式

首先,Python 程序是從上往下順序執行的,並且碰到函數的定義代碼塊是不會當即執行的,只有等到該函數被調用時,纔會執行其內部的代碼塊。關於這一點,其實咱們在前面的章節已經介紹過了。安全

def foo():
    print("foo函數被運行了!")

# 若是就這麼樣,foo 裏的語句是不會被執行的。 # 程序只是簡單的將定義代碼塊讀入內存中。 # foo() 只有調用了,纔會執行

其次,因爲順序執行的緣由,若是你真的對同一個函數定義了兩次,那麼,後面的定義會覆蓋前面的定義。所以,在 Python 中代碼的放置位置是有區別的,不能隨意擺放,一般函數體要放在調用的語句以前。架構

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

foo()

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

foo()
#---------------- 執行結果:
我是上面的函數定義!
我是下面的函數定義!

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

def outer(func):
    def inner():
        print("我是內層函數!")
    return inner

def foo():
    print("我是原始函數!")

outer(foo)
outer(foo())

函數名:  foo、outer、inner 框架

函數體:函數的整個代碼結構運維

返回值: return後面的表達式函數

函數的內存地址: id(foo)、id(outer) 等等學習

函數名加括號:對函數進行調用,好比 foo()、outer(foo) 

函數名做爲參數:  outer(foo) 中的 foo 自己是個函數,但做爲參數被傳遞給了outer 函數

函數名加括號被當作參數:其實就是先調用函數,再將它的返回值當作別的函數的參數,例如 outer(foo()) 

返回函數名: return inner 

返回函數名加括號: return inner() ,其實就是先執行inner函數,再將其返回值做爲別的函數的返回值。

若是你能理解函數也是一個對象,就能很容易地理解上面的概念。

有了這些基本的概念,咱們就能夠經過一個實例來說解 Python 中裝飾器的做用了。下例是針對函數的裝飾器。

虛擬場景

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

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

# 各部門分別調用本身須要的 API
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()

第四天:主管又換了個開發工程師。他是這麼幹的:定義個認證函數,在原來其餘的函數中調用它,代碼以下。

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()

可是主管依然不滿意,不過這一次他解釋了爲何。主管說:寫代碼要遵循開放封閉原則,簡單來講,已經實現的功能代碼內部不容許被修改,但外部能夠被擴展。若是將開放封閉原則應用在上面的需求中,那麼就是不容許在函數f1 、f二、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()

使用裝飾器 @outer,也是僅需對基礎平臺的代碼進行拓展,就能夠實如今其餘部門調用函數 API 以前都進行認證操做,在操做結束後保存日誌,而且其餘業務部門無需對他們本身的代碼作任何修改,調用方式也不用變。

裝飾器機制分析

下面以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所定義的函數。

  3. 程序返回到outer函數,開始執行裝飾器的語法規則。規則是:被裝飾的函數的名字會被看成參數傳遞給裝飾函數。裝飾函數執行它本身內部的代碼後,會將它的返回值賦值給被裝飾的函數。原來的f1函數被當作參數傳遞給了func,而f1這個函數名以後會指向inner函數。

 

                         

          注意:@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 嘛!號稱「萬能參數」!簡單修改一下上面的代碼:

ef 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數據接口......
日誌添加成功

裝飾器的學問博大精深,須要咱們不斷的學習思考。官方文檔和框架源碼是比較好的學習對象。

轉載:http://www.liujiangblog.com/course/python/39

相關文章
相關標籤/搜索