python3進階篇(二)——深析函數裝飾器

python3進階篇(二)——深析函數裝飾器

前言:
閱讀這篇文章我能學到什麼?
  裝飾器可以算python3中一個較難理解的概念了,這篇文章由淺入深帶你理解函數裝飾器,請閱讀它。

——如果您覺得這是一篇不錯的博文,希望你能給一個小小的贊,感謝您的支持。

1 裝飾器基本概念

  裝飾器是能夠修改已定義函數功能的函數,也即裝飾器本身就是具有這種特殊功能的 函數 。修改是有限制的修改,它只能在你定義的函數執行前或執行後執行其他代碼,當然也能給你的函數傳遞參數。不能直接修改你定義的函數內部的代碼。
  舉個通俗的例子。比如你定義了函數A,那麼函數A被裝飾器裝飾之後,此時你調用A,它可能先去執行一些動作(代碼)然後執行函數A,完了又去執行另外一些動作(代碼)。也即裝飾器幫你的函數附加了一些動作(代碼)執行,它改變了你原來定義的函數功能。
  如果你看過我的上一篇進階篇關於函數的講解(其中降到了函數嵌套定義、函數作爲參數、函數返回函數等問題),那麼後續的內容將會更容易理解。

2 創建裝飾器

2.1 創建裝飾器並且不使用@符號

  先不考慮使用@符號。我們從裝飾器的功能出發,嘗試自己去實現這樣功能的函數,這樣有助於我們理解裝飾器的作用。
代碼示例:

def Decorator(func):                            #裝飾器,對函數進行裝飾
    print("Call Decorator")
    def Func():
        print("Call Func")
        print("Before do something")
        func()
        print("After do something")

    return Func                                 #返回內部嵌套定義的函數地址

def Function():                                 #功能函數
    print("Call Function")

Function()                                      #裝飾前調用
print(Function)                                 #引用指向函數Function
print("------------------------------------")
Function = Decorator(Function)                  #對功能函數進行裝飾
print("------------------------------------")
print(Function)                                 #引用指向函數Func
Function()

運行結果:

Call Function
<function Function at 0x0000026E332D04C0>
------------------------------------
Call Decorator
------------------------------------
<function Decorator.<locals>.Func at 0x0000026E332D0550>
Call Func
Before do something
Call Function
After do something

  Function是我們的功能函數,我們按照自己的需求進行函數定義,它的功能是明確的,在裝飾器“裝飾”前我們進行調用,它的功能確定是輸出字符串Call Function。我們此時輸出函數地址爲0x0000026E332D04C0。函數Decorator很特殊,它以函數作爲參數func。內部嵌套定義了一個函數Func,在這個函數裏調用通過參數傳遞進來的外部函數,需要注意的是內部函數Func在外部函數func調用的前後都添加了自己的代碼邏輯,沒錯,這部分就是裝飾器裝飾上的內容。最後Decorator函數返回的是內部函數Func的函數地址即0x0000026E332D0550。其實Decorator就是裝飾器。
  仔細一想,Decorator這個函數非常有意思,輸入外部函數,在其內部函數中調用執行外部函數,並且在這個外部函數前後都添加上附加的代碼,然後返回內部函數的地址方便外部調用這個內部函數。
  注意Function = Decorator(Function)這裏是將函數地址作爲參數傳入,然後返回的函數地址賦值給變量,也即引用Function一開始指向的是Function函數,得裝飾器返回值後指向的是裝飾器內的函數Func

2.2 一種裝飾器給多個函數進行裝飾

  裝飾器創建好後可以對外部函數進行裝飾,對被裝飾的函數並沒有進行限制。所以我們可以對多個完全不同的函數使用同一個裝飾器對其裝飾。
代碼示例:

def Decorator(func):                            #裝飾器,對函數進行裝飾
    print("Call Decorator")
    def Func():
        print("Call Func")
        print("Before do something")
        func()
        print("After do something")

    return Func                                 #返回內部嵌套定義的函數地址

def Function1():                                #功能函數
    print("Call Function1")

def Function2():                                #功能函數
    print("Call Function2")

Function1 = Decorator(Function1)                #對功能函數進行裝飾
print("---------------------------")
Function2 = Decorator(Function2)                #對功能函數進行裝飾
print("---------------------------")

Function1()                                     #裝飾後調用
print("---------------------------")
Function2()                                     #裝飾後調用

運行結果:

Call Decorator
---------------------------
Call Decorator
---------------------------
Call Func
Before do something
Call Function1
After do something
---------------------------
Call Func
Before do something
Call Function2
After do something

  同一個裝飾器對不同的Function1Function2函數進行了同一種“裝飾”。

3 裝飾器@符號的使用

3.1 使用@符號裝飾函數

  @+裝飾器名然後跟函數定義,表明定義的函數被指定的裝飾器進行裝飾。我們回顧一下上面沒有使用@符號的時候是怎麼對Function函數進行裝飾的。首先我們定義了裝飾器函數Decorator,然後通過調用這個裝飾器函數即Function = Decorator(Function),這句指明瞭Function函數被裝飾器Decorator修飾,並且最後引用變量還是使用Function。也就是說功能函數Function的定義和它被裝飾器“裝飾”是分開的。Function定義後裝飾前,我們還能調用到沒有被裝飾過的Function函數。使用@符號進行裝飾的作用在於函數完成定義後就立即被指定的裝飾器裝飾(你沒法再調用到沒有被裝飾時的狀態),另外就是寫法上簡潔統一(函數定義和裝飾在一個位置)。
代碼示例:

def Decorator(func):                          #裝飾器,對函數進行裝飾
    print("Call Decorator")
    def Func():
        print("Call Func")
        print("Before do something")
        func()
        print("After do something")

    return Func

@Decorator                                    #使用裝飾器對函數Function進行裝飾,函數完成定義後就立刻進行了裝飾
def Function():
    print("Call Function")

print("----------------------")
Function()                                    #裝飾後調用函數

運行結果:

Call Decorator
----------------------
Call Func
Before do something
Call Function
After do something

  在不使用@進行函數裝飾時,在裝飾前我們依舊可以調用到沒有經過“裝飾”的Function函數,而使用@在函數定義處完成了裝飾。不管使用哪種方法,我們都需要定義好裝飾器函數。

3.2 使用@符號讓一個裝飾器裝飾多個函數

  同樣的,使用@符號在幾個不同函數的定義處都可以指明被同一個裝飾器裝飾(裝飾器是可以複用的)。
代碼示例:

def Decorator(func):                          #裝飾器,對函數進行裝飾
    print("Call Decorator")
    def Func():
        print("Call Func")
        print("Before do something")
        func()
        print("After do something")

    return Func

@Decorator
def Function1():
    print("Call Function1")

@Decorator
def Function2():
    print("Call Function2")

print("------------------------------------")
Function1()
print("------------------------------------")
Function2()

運行結果:

Call Decorator
Call Decorator
------------------------------------
Call Func
Before do something
Call Function1
After do something
------------------------------------
Call Func
Before do something
Call Function2
After do something

  以上你已經學會創建一個真正的裝飾器並使用它。但還有些美中不足的地方。

3.3 被裝飾函數的__name__和__doc__參數

  python3的函數有一些內置參數,比如__name__存儲函數名(在定義時決定),__doc__用於保存函數的描述信息(這個在基礎篇函數章節有講過)。
代碼示例:

def add(a, b):
    "Get the sum of two numbers"              #函數的描述信息
    return a + b

Fun = add

print(add.__name__)
print(add.__doc__)
print("----------------------------")
del add
print(Fun.__name__)                           #函數名已經是add
#print(add.__name__)                          #error: NameError: name 'add' is not defined

運行結果:

add
Get the sum of two numbers
----------------------------
add

  可以看到,即使給函數創建新的引用,用新的應用輸出函數名print(Fun.__name__),函數名也依舊是add,也即函數名在函數定義時確定,和引用變量名無關。我們將最初的引用add進行刪除del add,此時不能繼續通過add訪問函數的屬性,但是可以繼續通過其他引用訪問。
  說了這麼多就是想告訴你,函數定義時就決定了一些函數的屬性信息,這些信息會保存在python內置的函數屬性裏。裝飾器是將一個外部函數輸入,輸出自己的內部函數,然後引用變量名不變,但實際指向的已經不是原來那個函數了。會存在一個問題,定義的功能函數經過裝飾器後,函數的屬性信息都變了,變成內部函數的。python還有很多其他的函數內置屬性,這裏我們只以__name____doc__屬性舉例。
代碼示例:

def Decorator(func):                          #裝飾器,對函數進行裝飾
    "Description: Decorator"
    def Func():
        "Description: Func"
        print("Before do something")
        func()
        print("After do something")

    return Func

@Decorator
def Function():
    "Description: Function"
    print("Call Function1")

print(Function.__name__)
print(Function.__doc__)

運行結果:

Func
Description: Func

  輸出的並不是Function定義時的信息,而是內部嵌套函數Func的信息。所以爲了追求這一點完美,我們需要進行一些優化。
  python爲我們提供了滿足這種需求的方法,即將函數的屬性信息替換爲原來的。
代碼示例:

from functools import wraps

def Decorator(func):                          #裝飾器,對函數進行裝飾
    "Description: Decorator"
    @wraps(func)
    def Func():
        "Description: Func"
        print("Before do something")
        func()
        print("After do something")

    return Func

@Decorator
def Function():
    "Description: Function"
    print("Call Function1")

print(Function.__name__)
print(Function.__doc__)

運行結果:

Function
Description: Function

  我們導入了wraps模塊,並且將內部函數Func用裝飾器wraps進行了裝飾,進過這一次裝飾後將函數的屬性信息替換回了原來的。注意下,這裏的裝飾器wraps是帶參數的,傳遞進外部函數地址。後面我們繼續講帶參數的裝飾器。
  好了,現在你已經大致理解了裝飾器的概念和作用了,我們繼續看更深入的用法。

4 帶參的裝飾器

  如果看過我上一篇進階篇關於函數的深入講解就很容易能理解裝飾器怎麼實現帶參數的。回顧一下上面不帶參數的構造器,我們對裝飾器函數內進行了嵌套定義,也即它是兩層函數,如果我們繼續在外面嵌套上一層,利用外面這層函數的參數列表就能實現裝飾器的帶參了。當然它的返回值也必須是內層函數的地址,這樣才能實現遞推式的調用。

4.1 創建帶參裝飾器並且不使用@符號

  同樣的,我們先不適用@進行帶參裝飾器創建,這樣有利於我們理解@符號到底做了什麼動作。
代碼示例:

from functools import wraps

def Operation(Parameter):                                               #這裏的參數列表就是裝飾器的參數列表
    def OperationDecorator(OutFunc):                                    #裝飾器,對函數進行裝飾
        @wraps(OutFunc)
        def InFunc(Num1, Num2):                                         #含有外部函數的參數列表
            print("Before do something")
            print(Parameter)
            Ret = OutFunc(Num1, Num2)                                   #調用外部函數
            print("After do something")
            return Ret                                                  #內部函數返回值就是函數調用最終的返回值

        return InFunc
    return OperationDecorator

def add(Num1, Num2):
    return Num1 + Num2

add = Operation("Calculate the sum of two numbers")(add)                #帶參數裝飾器

print(add(1, 2))

運行結果:

Before do something
Calculate the sum of two numbers
After do something
3

  相比於之前不帶參數的裝飾器,我們在多嵌套了一層函數Operation,這個函數具有參數,並且其返回值爲內層函數OperationDecorator,這層函數的作用就是我們上面講過的將外部函數地址替換爲內部函數地址。所以就不難理解add = Operation("Calculate the sum of two numbers")(add)這行代碼的作用了,先調用Operation函數並給其傳入參數,該函數返回OperationDecorator函數地址,此時函數地址+(add)就又構成了函數調用,此時就和不帶參裝飾器用法相同了,add引用最終指向了內部函數InFunc,在內部函數裏可以拿到我們給裝飾器傳入的參數Parameter(內層函數可以訪問外層函數的變量)。

4.2 使用@符號創建帶參裝飾器

  @符號的作用不變,讓創建迭代器寫法更簡潔,讓函數在的定義和裝飾在一處。
代碼示例:

from functools import wraps

def Operation(Parameter):                                           #這裏的參數列表就是裝飾器的參數列表
    def OperationDecorator(OutFunc):                                #裝飾器,對函數進行裝飾
        @wraps(OutFunc)
        def InFunc(Num1, Num2):                                     #含有外部函數的參數列表
            print("Before do something")
            print(Parameter)
            Ret = OutFunc(Num1, Num2)                               #調用外部函數
            print("After do something")
            return Ret                                              #內部函數返回值就是函數調用最終的返回值

        return InFunc
    return OperationDecorator


@Operation("Calculate the sum of two numbers")                      #帶參數裝飾器
def add(Num1, Num2):
    return Num1 + Num2

print(add(1, 2))

運行結果:

Before do something
Calculate the sum of two numbers
After do something
3

  這就是帶參裝飾器了,需要注意一點。帶參也可以是無參數的,什麼意思?就是說嵌套了三層函數,最外層的函數也可以是無參,但是此時無參也不能省略()符號。
代碼示例:

from functools import wraps

def Operation():                                                    #這裏的參數列表就是裝飾器的參數列表
    def OperationDecorator(OutFunc):                                #裝飾器,對函數進行裝飾
        @wraps(OutFunc)
        def InFunc(Num1, Num2):                                     #含有外部函數的參數列表
            print("Before do something")
            Ret = OutFunc(Num1, Num2)                               #調用外部函數
            print("After do something")
            return Ret                                              #內部函數返回值就是函數調用最終的返回值

        return InFunc
    return OperationDecorator


@Operation()                                                        #帶參數裝飾器
def add(Num1, Num2):
    return Num1 + Num2

print(add(1, 2))

運行結果:

Before do something
After do something
3

  這裏的@Operation()不能寫作@Operation,即使它沒有參數,因爲這種形式是嵌套了三層函數。

5 裝飾器類

  爲了更好的封裝性,我們可以將裝飾器封裝爲一個類,但是通過@符號的用法確保持不變。所謂裝飾器類就是把裝飾器定義在類中。用到了類的兩個方法。一個是類的構造函數__init__,構造函數的參數列表就是裝飾器的參數列表,帶參情況取決於構造函數。另一個是__call__方法,該方法使得實例化的對象可以被直接訪問,通俗點說正常情況下訪問類的方法是通過對象名+.``方法名,而直接訪問對象就爲對象名+()參數列表,直接訪問時執行的就是__call__方法。另外還需要注意的是,使用裝飾器類裝飾函數,在函數定義並裝飾時就調用了__init____call__方法。
代碼示例:

from functools import wraps

class cDecorator:
    def __init__(self, Parameter):                      #構造函數的參數列表就是裝飾器的傳輸列表
        print("Call __init__")
        self.Parameter = Parameter
        pass

    def __call__(self, func):                           #call函數將實例化的對象可直接調用
        print("Call __call__")
        @wraps(func)
        def Func(Num1, Num2):
            print("Call Func")
            print(self.Parameter)                       #訪問成員變量,值爲裝飾器輸入參數
            self.FuncA()                                #訪問成員函數
            return func(Num1, Num2)

        return Func

    def FuncA(self):
        print("call FuncA")
        pass

@cDecorator("A")                                        #帶參裝飾器
def add(Num1, Num2):
    print("Call add")
    return Num1 + Num2

print("----------------------------")
print(add(1, 2))

運行結果:

Call __init__
Call __call__
----------------------------
Call Func
A
call FuncA
Call add
3

  需要留意這些函數的執行順序以及什麼時候被執行(是在裝飾時執行還是在被裝飾函數調用時執行)。

6 多層裝飾

  一個函數可以反覆被多次裝飾,即對裝飾過得函數再進行裝飾。我們可以使用不同的裝飾器去裝飾一個函數,也可以用同一個裝飾器重複裝飾器一個函數,但原理都是一樣的,下面我們用前面帶參數裝飾器的例子,對同一個函數用同一個裝飾器裝飾兩次來說明這種方法。
代碼示例:

from functools import wraps

def Operation(Parameter):                                           #這裏的參數列表就是裝飾器的參數列表
    print("Call Operation", Parameter)
    def OperationDecorator(OutFunc):                                #裝飾器,對函數進行裝飾
        print("Call Operation_Decorator", Parameter)
        @wraps(OutFunc)
        def InFunc(Num1, Num2):                                     #含有外部函數的參數列表
            print("Before do something", Parameter)
            print(Parameter)
            Ret = OutFunc(Num1, Num2)                               #調用外部函數
            print("After do something", Parameter)
            return Ret                                              #內部函數返回值就是函數調用最終的返回值

        return InFunc
    return OperationDecorator


@Operation("A")                      #帶參數裝飾器
@Operation("B")                      #帶參數裝飾器
def add(Num1, Num2):
    return Num1 + Num2

print("---------------------------------")
print(add(1, 2))

運行結果:

Call Operation A
Call Operation B
Call Operation_Decorator B
Call Operation_Decorator A
---------------------------------
Before do something A
A
Before do something B
B
After do something B
After do something A
3

  相信從運行結果你已經看出規律來了,這就跟拿袋子套東西一個道理,我們先給功能函數套上一層B,然後在套上一層A。假設功能函數的執行我們記爲F,當我們給他套上一層B時,功能函數執行前後就多了一層B的裝飾,即 BFB,然後繼續套上A變爲 ABFBA,更多層的嵌套同理。不管套多少層中間最核心的F只會執行一次,函數只會有一個返回值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章