1. 程式人生 > >python語言基礎十

python語言基礎十


使用裝飾器需要注意的地方 : 


裝飾器其實【 本質 】 : 源於【閉包的函式】,這個閉包函式 【將一個函式作為引數傳入】,然後 【返回一個替代版的函式】 。

兩個主要的概念  【閉包】 、 【替換】 。

一、關於閉包 :
    閉包(Closure),又稱詞法閉包(Lexical Closure)或函式閉包(Function Closures),是引用了自由變數的函式。
    這個【被引用的自由變數】將【和這個函式一同存在】,【即使已經離開了創造它的環境也不例外】。
    所以,有另一種說法認為【閉包】是【由函式】和【與其相關的引用環境】【組合而成的實體】,
    這種組合使用一種對映關係將函式的自由變數(在 local 使用但是在 enclosing scope 使用的變數)與函式相關聯。


二、 在定義裝飾器的時候,裝飾器名字對於的函式需要接受一個引數(一般都是一個函式), 
    在裝飾器的內層函式裡面需要對外部函式進行呼叫(內層函式不能接受外部的這個函式作為引數)


# deco 函式接收另一個函式作為引數
def deco(func):
    print('我是deco的頭')

    # 在 deco 函式內部定義了另外一個函式 wrappend,(wrappend可以訪問 deco 函式裡面的資料, 但是不能直接修改)
    def wrappend():                            
        print("我是wrappend的頭")
        print("我是wrappend的屁股")

        # 在 wrappend 裡面訪問 外部函式的資料 func, 呼叫這個函式,並且把他的結果作為返回值
        return func() + 1

    print('我是deco的屁股')

    # 
    return wrappend

def foo():
    print("我要被裝飾器裝飾")
    return 1

print(foo())
print('割-----------------割')

print(deco(foo)())
print('割-----------------割')
print(type(deco(foo)))                    # 輸出 <class 'function'> , 證明deco 返回的是一個函式

輸出如下 : 

我要被裝飾器裝飾
1
割-----------------割
我是deco的頭
我是deco的屁股
我是wrappend的頭
我是wrappend的屁股
我要被裝飾器裝飾
2
割-----------------割
我是deco的頭
我是deco的屁股
<class 'function'>

解析 : 

首先定義一個函式 deco , 在deco 函式內部再定義一個 wrappend 函式, 此時 deco 函式內部, wrappend 函式外部,
就是 wrappend 的【閉包範圍】, wrappend 會記錄到環境內的關係,同時根據 LEGB 搜尋原則找尋引數。 因此再 return 的
時候可以到 func 函式。
【注】: 此處的 deco 函式的主要作用在於, 對傳入的 func 做一定處理後, 返回一個新的函式wrappend 作為替代,
隨後定義一個 foo 函式作為傳入的引數, 通過輸出語句可以看到, 經過 deco 函式裝飾的 foo 函式在不改動內部函式的前提下, 
完成了對函式行為的改變, 這樣的一個 deco 函式, 就是裝飾器的本質起源。


三、 語法糖

    前面介紹的通過裝飾器本質的方法使用裝飾器依舊存在一個麻煩需要處理,也就是當需要對所有的被裝飾函式都進行相同的
    裝飾時, 需要在每個呼叫的位置都呼叫一次裝飾函式, 那麼這樣無疑增加了程式碼的修改難度。

    為此便出現了 語法糖 @ , 利用語法糖可以很方便的裝飾一個函式, 只需要在被裝飾的函式之上使用 @deco 便
    可以將函式 deco 裝飾到被裝飾的函式 foo 上,而且當再次呼叫 foo 時, 會首先呼叫一次 foo=deco(foo), 
    自動完成對函式的裝飾 。


def deco(func):
    print('我是deco的頭')

    def wrappend():
        print("我是wrappend的頭")
        print("我是wrappend的屁股")
        return func() + 1

    print('我是deco的屁股')

    return wrappend

@deco
def foo():
    print("我要被裝飾器裝飾")
    return 1
print(foo())

同時, 語法糖也是可以疊加的, 呼叫的順序與宣告的順序是相反的, 即自下而上。

@deco
@deco
def foo():
    print("我要被裝飾器裝飾")
    return 1
print(foo())


四、裝飾器傳入引數

    裝飾器的引數傳入主要有兩種, 
    一種是能夠裝飾需要傳入引數的函式,
    第二種是裝飾器自身引數。

    首先介紹如何使裝飾器裝飾帶引數傳入的函式, 最直接的方式是定義返回函式 wrappend時, 
    將其定義成與被裝飾函式 func 相同的形參結構, 這樣返回的 wrappend 函式便能夠接受 func 的引數傳入。
    利用裝飾器函式和語法糖分別進行驗證嗎可以得到期望的結果。

def deco(func):
    print('我是deco的頭')

    def wrappend(a, b):
        print("我是wrappend的頭")
        print("我是wrappend的屁股")
        return func(a, b) + 1

    print('我是deco的屁股')

    return wrappend

@deco
def foo(a, b):
    print("我要被裝飾器裝飾")
    return a + b

此時 foo = deco(foo()) = wrappend
所以 foo(1, 2) = wrappend(1, 2) = a + b + 1
print(foo(1, 2))


但是上面的方法有侷限性, 當我們不知道被裝飾的函式需要傳入多少引數的時候, 便無法定義內部的返回函式 wrappend 。
同時,當被裝飾函式的輸入不同時, 裝飾器的通用性便被破壞。為此, 可以利用 運算子 * 和 ** 來對任意引數進行元組或
字典的解包, 從而完成一個通用的裝飾器。 


def deco(func):
    print('我是deco的頭')

    def wrappend(*args, **kwargs):
        print("我是wrappend的頭")
        print("我是wrappend的屁股")
        return func(*args, **kwargs) + 1

    print('我是deco的屁股')

    return wrappend


@deco
def foo(a, b, c):
    print("我要被裝飾器裝飾")
    return a + b + c


print(foo(1, 2, c=10))


同樣的, 對於裝飾器, 也可以傳入引數, 可以利用傳入的引數完成諸如裝飾器開關的操作(設定為True或False來確定
返回 wrappend 還是返回 原來的foo函式)。要實現這個功能, 則需要在裝飾器的外面再多一層函式。
如下的 decora 函式實質上是起到返回裝飾器函式 decor 的作用,語法糖修飾的 @decora(3) 實際上等價於 @decor,
因為 decora(3) 返回的是一個函式 decor 。 同時, 在此處也體現了函式必閉包的特性, 
即在內層函式中記錄了閉包環境中的引數 c 的值。


def decora(c):
    def deco(func):
        print('我是deco的頭')

        def wrappend(*args, **kwargs):
            print("我是wrappend的頭")
            print("我是wrappend的屁股")
            return func(*args, **kwargs) + c

        print('我是deco的屁股')

        return wrappend
    return deco


@decora(3)
def foo(a, b, d):
    print("我要被裝飾器裝飾")
    return a + b + d


print(foo(1, 2, d=10))

--------------------------------------


有一點要特別注意 : 
我們寫好一個裝飾器函式, 然後用它來裝飾另外一個函式的時候, 我們的裝飾器函式就已經被呼叫了, 
我們被裝飾的函式的函式名 也已經指向了 裝飾器函式所返回的 wrappend 函數了!!!

所以啊所以, 當我們在呼叫被裝飾的函式的時候, 實際上呼叫的是 wrappend 函式!!, 
然後再在 wrappend 函式裡面呼叫我們之前定義的被裝飾器裝飾的函式, 我們的裝飾器函式並不會執行。

如下 :

def deco(func):
    print('我是deco的頭')

    def wrappend(a, b):
        print("我是wrappend的頭")
        print("我是wrappend的屁股")
        return func(a, b) + 1

    print('我是deco的屁股')

    return wrappend

@deco
def foo(a, b):
    print("我要被裝飾器裝飾")
    return a + b


print('-'*50)

print(foo(1, 5))
print('-'*50)

print(foo(1, 5))

輸出 : 
我是deco的頭
我是deco的屁股
--------------------------------------------------
我是wrappend的頭
我是wrappend的屁股
我要被裝飾器裝飾
7
--------------------------------------------------
我是wrappend的頭
我是wrappend的屁股
我要被裝飾器裝飾
7