1. 程式人生 > 其它 >(轉)你不一定全知道的四種Python裝飾器實現詳解

(轉)你不一定全知道的四種Python裝飾器實現詳解

原文:https://markdowner.net/article/157600452270493696

一、引言

老猿一直想寫一篇比較完整的裝飾器介紹的博文,從開始寫到現在至少過去有半年了,一直都還未寫完,因為還有一些知識點沒有研究透徹,因此一直在草稿箱放著。在寫這個材料的時候,發現Python中的裝飾器網上介紹的材料很多,大多數都是介紹的裝飾器函式,但也有極少數介紹了裝飾器類或用裝飾器函式裝飾類。老猿想來,裝飾器按照裝飾器本身和被裝飾物件來區分類和函式,應該有4種組合,前面說的有三種,應該還有一種裝飾器和被裝飾物件都是類的組合。但公開資料中未查到是否可以有類的類裝飾器,即裝飾器和被裝飾物件都是類。老猿參考類的函式裝飾器、函式的類裝飾器做了很多測試,一度以為沒辦法實現類的類裝飾器,準備放棄,隔了很長一段時間後,最近又花了兩天時間進行研究測試,終於弄通了。基於上述研究,老猿決定先單獨寫一篇關於裝飾器四種類型的詳細介紹。

裝飾器的概念就不介紹了,按照裝飾器的型別、被裝飾物件的型別,老猿將裝飾器分為如下四種:

  1. 函式的函式裝飾器:裝飾器和被裝飾物件都為函式;
  2. 類的函式裝飾器:裝飾器為函式,被裝飾物件為類;
  3. 函式的類裝飾器:裝飾器為類,被裝飾物件為函式;
  4. 類的類裝飾器:裝飾器和被裝飾物件都為類。

二、函式的函式裝飾器

裝飾器包含裝飾物件和被裝飾物件,最簡單的裝飾器是用裝飾器函式裝飾被裝飾函式,在這種場景下,裝飾器為函式裝飾器,被裝飾物件也是函式。

2.1、概述

函式裝飾器就是一個特殊的函式,該函式的引數就是一個函式,在裝飾器函式內重新定義一個新的函式,並且在其中執行某些功能前後或中間來使用被裝飾的函式,最後返回這個新定義的函式。裝飾器也可以稱為函式的包裝器,實際上就是在被裝飾的函式執行前或後增加一些單獨的邏輯程式碼,以使得被裝飾函式執行後最終的結果受到裝飾函式邏輯的影響以改變或限定被裝飾函式的執行結果。

2.2、裝飾器定義語法


@decoratorName
def originalFunction(*args,**kwargvs):

    函式體

2.3、裝飾器語法解釋

  • 裝飾器的定義是以@符號開頭來宣告的
  • decoratorName是裝飾器的名字,decoratorName必須對應存在一個封閉函式(請參考《第13.2節 關於閉包》的介紹),該封閉函式滿足如下要求:
  1. 引數是一個函式物件;
  2. 封閉函式內部存在一個巢狀函式,該巢狀函式內會呼叫封閉函式引數指定的函式,並新增額外的其他程式碼(這些程式碼就是裝飾);
  3. 巢狀函式的引數必須包含originalFunction的引數,但不能帶被裝飾物件originalFunction;
  4. 巢狀函式返回值必須與封閉函式引數指定函式的返回值類似,二者符合鴨子型別要求(關於鴨子型別請參考《第7.3節 Python特色的面向物件設計:協議、多型及鴨子型別》);
  5. 封閉函式的返回值必須是巢狀函式。
  • 裝飾器函式的定義參考如下形式:

def decoratorName(originalFunction,*args,**kwargvs):

    def closedFunction(*args,**kwargvs):
        ...  #originalFunction函式執行前的一些裝飾程式碼
        ret = originalFunction(*args,**kwargvs)
        ... #originalFunction函式執行的一些裝飾程式碼
        return ret

    return closedFunction

其中decoratorName是裝飾器函式,originalFunction是被裝飾的函式,closedFunction是裝飾器函式內的巢狀函式。

  • 裝飾器定義的語法本質上等同於如下語句:

    originalFunction = decoratorName(originalFunction)

2.4、多層裝飾器的使用

在一個函式外,可以順序定義多個裝飾器,類似如:


@decorator1
@decorator2
@decorator3
def originalFunction(*args,**kwargvs):
    函式體

這種多個裝飾器實際上就是疊加作用,且在上面的裝飾器是對其下裝飾器的包裝,以上定義語句效果等同於如下語句:


originalFunction = decorator3(originalFunction)
originalFunction = decorator2(originalFunction)
originalFunction = decorator1(originalFunction)

也即等價於:


originalFunction = decorator1(decorator2(decorator3(originalFunction)))

三、類的函式裝飾器

3.1、定義

函式裝飾器除了給函式加裝飾器(使用函式名作為裝飾器函式的引數)外,還可以給類加函式裝飾器,給類加函式裝飾器時,將類名作為裝飾器函式的引數,並在裝飾器函式內定義一個類如wrapClass,該類稱為包裝類,包裝類的建構函式中必須呼叫被裝飾類來定義一個例項變數,裝飾器函式將返回包裝類如wrapClass。

3.2、類的函式裝飾器案例1


def decorateFunction(fun, *a, **k):
    class wrapClass():
        def __init__(self, *a, **k):
            self.wrappedClass=fun(*a, **k)

        def fun1(self,*a, **k):
            print("準備呼叫被裝飾類的方法fun1")
            self.wrappedClass.fun1(*a, **k)
            print("呼叫被裝飾類的方法fun1完成")

    return wrapClass

@decorateFunction
class wrappedClass:

    def __init__(self ,*a, **k):
        print("我是被裝飾類的構造方法")
        if a:print("構造方法存在位置引數:",a)
        if k:print("構造方法存在關鍵字引數:",k)
        print("被裝飾類構造方法執行完畢")

    def fun1(self,*a, **k):
        print("我是被裝飾類的fun1方法")
        if a:print("fun1存在位置引數:",a)
        if k:print("fun1存在關鍵字引數:",k)
        print("被裝飾類fun1方法執行完畢")

    def fun2(self,*a, **k):
        print("我是被裝飾類的fun2方法")

針對以上被裝飾函式裝飾的類wrappedClass,我們執行如下語句:


>>> c1 = wrappedClass('testPara',a=1,b=2)
我是被裝飾類的構造方法
構造方法存在位置引數: ('testPara',)
構造方法存在關鍵字引數: {'a': 1, 'b': 2}
被裝飾類構造方法執行完畢

>>> c1.fun1()
準備呼叫被裝飾類的方法fun1
我是被裝飾類的fun1方法
被裝飾類fun1方法執行完畢
呼叫被裝飾類的方法fun1完成

>>> c1.fun2()
Traceback (most recent call last):
  File "<pyshell#37>", line 1, in <module>
    c1.fun2()
AttributeError: 'wrapClass' object has no attribute 'fun2'

>>>

可以看到被裝飾類的相關方法必須在裝飾類中呼叫才能執行,裝飾後的類如果裝飾函式定義類時未定義被裝飾類的同名函式,在裝飾後返回的類物件無法執行被裝飾類的相關方法。

3.3、類的函式裝飾器案例2

上面的案例1是通過將被裝飾類的方法在裝飾器函式內部的裝飾類中靜態重新定義方式來實現對被包裝類方法的支援,這種情況可以用於裝飾器裝飾後的類只需呼叫指定已知方法,但有時我們的裝飾器可能用於裝飾多個類,只針對構造方法和特定方法在裝飾類中重寫會導致被裝飾類需要呼叫的功能不能呼叫,這時我們需要在裝飾器中實現一個通用方法來保障被裝飾類裝飾後能執行被裝飾類的所有方法。這就需要藉助setattr進行類例項方法的動態定義。


def decorateFunction(fun, *a, **k):
    class wrapClass():
        def __init__(self, *a, **k):
            self.wrappedClass=fun(*a, **k)
            self.decorate() #針對沒有重寫定義的方法賦值給wrapClass作為例項變數,本案例中為涉及的為fun2方法

        def fun1(self,*a, **k):
            print("準備呼叫被裝飾類的方法fun1")
            self.wrappedClass.fun1(*a, **k)
            print("呼叫被裝飾類的方法fun1完成")

        def decorate(self):#針對沒有重寫定義的方法賦值給wrapClass作為例項變數

            for m in dir(self.wrappedClass):
                if not m.startswith('_')and m!='fun1':
                    fn = getattr(self.wrappedClass, m)
                    if callable(fn):
                        setattr(self, m,fn)

    return wrapClass

@decorateFunction
class wrappedClass:
    def __init__(self ,*a, **k):
        print("我是被裝飾類的構造方法")
        self.name = a[0]
        if a:print("構造方法存在位置引數:",a)
        if k:print("構造方法存在關鍵字引數:",k)
        print("被裝飾類構造方法執行完畢")        

    def fun1(self,*a, **k):
        print("我是被裝飾類的fun1方法")   
        if a:print("fun1存在位置引數:",a)
        if k:print("fun1存在關鍵字引數:",k)
        print("我的例項名字為:",self.name)
        print("被裝飾類fun1方法執行完畢")

    def fun2(self,*a, **k):
        print("我是被裝飾類的fun2方法")
        if a:print("fun2方法存在位置引數:",a)
        if k:print("fun2存在關鍵字引數:",k)
        print("我的例項名字為:",self.name)

針對以上被裝飾函式裝飾的類wrappedClass,我們執行如下語句:


>>> c1 = wrappedClass('c1',a=1,b=2)
我是被裝飾類的構造方法
構造方法存在位置引數: ('c1',)
構造方法存在關鍵字引數: {'a': 1, 'b': 2}
被裝飾類構造方法執行完畢

>>> c2 = wrappedClass('c2',a=12,b=22)
我是被裝飾類的構造方法
構造方法存在位置引數: ('c2',)
構造方法存在關鍵字引數: {'a': 12, 'b': 22}
被裝飾類構造方法執行完畢

>>> c1.fun1()
準備呼叫被裝飾類的方法fun1
我是被裝飾類的fun1方法
我的例項名字為: c1
被裝飾類fun1方法執行完畢
呼叫被裝飾類的方法fun1完成

>>> c2.fun2()
我是被裝飾類的fun2方法
我的例項名字為: c2

>>> c1.fun2()
我是被裝飾類的fun2方法
我的例項名字為: c1

>>>

可以看到,除了在裝飾類中重寫的fun1方法可以正常執行外,沒有重寫的方法fun2也可以正常執行。

四、函式的類裝飾器

除了用函式作為裝飾器裝飾函式或者裝飾類之外,也可以使用類作為函式的裝飾器。將類作為函式的裝飾器時,需將要裝飾的函式作為裝飾器類的例項成員,由於裝飾後,呼叫相關方法時實際上呼叫的是裝飾類的例項物件本身,為了確保類的例項物件可以呼叫,需要給類增加__call__方法。

案例:


class decorateClass:
    def __init__(self,fun):
        self.fun=fun

    def __call__(self, *a, **k):
        print("執行被裝飾函式")
        return self.fun( *a, **k)

@decorateClass 
def fun( *a, **k):
    print(f"我是函式fun,帶引數:",a,k)
    print("老猿Python部落格文章目錄:https://blog.csdn.net/LaoYuanPython/article/details/109160152,敬請關注同名微信公眾號")

定義後執行相關呼叫情況如下:


>>> f = fun('funcation1',a=1,b=2)
執行被裝飾函式
我是函式fun,帶引數: ('funcation1',) {'a': 1, 'b': 2}
老猿Python部落格文章目錄:https://blog.csdn.net/LaoYuanPython/article/details/109160152,敬請關注同名微信公眾號

>>>

五、類的類裝飾器

前面分別介紹了函式的函式裝飾器、類的函式裝飾器、函式的類裝飾器,但公開資料中未查到是否可以有類的類裝飾器,即裝飾器和被裝飾物件都是類。老猿參考類的函式裝飾器、函式的類裝飾器最終確認類的類裝飾器也是可以支援的。

5.1、實現要點

要實現類的類裝飾器,按老猿的研究,類的裝飾器類的實現需要遵循如下要點:

  1. 裝飾器類必須實現至少兩個例項方法,包括__init__和__call__
  2. 在裝飾器類的構造方法的引數包括self,wrapedClass,*a,**k,其中wrapedClass代表被裝飾類,a代表被裝飾類構造方法的位置引數,k代表被裝飾類構造方法的關鍵字引數。關於位置引數和關鍵字引數請參考《第五章函式進階 第5.1節 Python函式的位置引數、關鍵字引數精講》;
  3. 在裝飾器類的構造方法中定義一個包裝類如叫wrapClass,包裝類從裝飾器類的構造方法的引數wrapedClass(即被裝飾類)繼承,包裝類wrapClass的構造方法引數為self,*a,**k,相關引數含義同上;
  4. 在包裝類的構造方法中呼叫父類的構造方法,傳入引數a、k;
  5. 在裝飾器類的構造方法中用例項變數(例如self.wrapedClass)儲存wrapClass類;
  6. 在裝飾器類的__call__方法中呼叫self.wrapedClass(*a,**k)建立被裝飾類的一個物件,並返回該物件。

按照以上步驟建立的類裝飾器,就可以用於裝飾其他類。當然上述方法只是老猿自己研究測試的結論,是否還有其他方法老猿也不肯定。

5.2、類的類裝飾器案例


class decorateClass: #裝飾器類
    def __init__(self,wrapedClass,*a,**k): #wrapedClass代表被裝飾類
        print("準備執行裝飾類初始化")

        class wrapClass(wrapedClass):
            def __init__(self,*a,**k):
                print(f"初始化被封裝類例項開始,位置引數包括:{a}, 關鍵字引數為{k}")
                super().__init__(*a,**k)
                print(f"初始化被封裝類例項結束")

        self.wrapedClass=wrapClass
        print("裝飾類初始化完成")

    def __call__(self, *a, **k):
        print("被裝飾類物件初始化開始")
        wrapedClassObj = self.wrapedClass(*a,**k)
        print("被裝飾類物件初始化結束")
        return wrapedClassObj

@decorateClass
class car:
    def __init__(self,type,weight,cost):
        print("class car __init__ start...")
        self.type = type
        self.weight = weight
        self.cost = cost
        self.distance = 0
        print("class car __init__ end.")

    def driver(self,distance):
        self.distance += distance
        print(f"{self.type}已經累計行駛了{self.distance}公里")
        print("老猿Python部落格文章目錄:https://blog.csdn.net/LaoYuanPython/article/details/109160152,敬請關注同名微信公眾號")

c = car('愛麗舍','1.2噸',8)
c.driver(10)
c.driver(110)

執行以上程式碼,輸出如下:


準備執行裝飾類初始化
裝飾類初始化完成
被裝飾類物件初始化開始
初始化被封裝類例項開始,位置引數包括:('愛麗舍', '1.2噸', 8), 關鍵字引數為{}
class car __init__ start...
class car __init__ end.
初始化被封裝類例項結束
被裝飾類物件初始化結束
愛麗舍已經累計行駛了10公里
愛麗舍已經累計行駛了120公里
老猿Python部落格文章目錄:https://blog.csdn.net/LaoYuanPython/article/details/109160152,敬請關注同名微信公眾號

除了上述方法,老猿又找到了一種更簡單的方法,具體請參考《類的類裝飾器實現思路及案例》。

六、小結

本文詳細介紹了Python中的四類裝飾器,這四類裝飾器根據裝飾器和被裝飾物件的型別分為函式的函式裝飾器、類的函式裝飾器、函式的類裝飾器、類的類裝飾器,文中詳細介紹了四類裝飾器的實現步驟,並提供了對應的實現案例,相關介紹有助於大家全面及詳細地理解Python的裝飾器。

https://markdowner.net/article/157600452270493696

技術連結