[Python] 對 Python 裝飾器的理解心得


  最近寫一個py腳本來整理電腦中的文檔,其中需要檢校輸入的字符,為了不使代碼冗長,想到使用裝飾器。

  上網搜索有關python的裝飾器學習文檔,主要看的是AstralWind的一篇博文,以及Limodou的一篇文章。作為初學者,這兩篇文章對新手有很大的幫助,但仍然有些不易理解的地方。因此在此以一個初學者的認知記錄一下python的裝飾器的學習心得。

 

  1. 什么是裝飾器?

 

  顧名思義,裝飾器就是在方法上方標一個帶有@符號的方法名,以此來對被裝飾的方法進行點綴改造。

  當你明白什么是裝飾器之后,自然會覺得這個名字取得恰如其分,但作為初學者來說多少還是會有些迷茫。下面用代碼來說明怎么理解裝飾器。

 

#腳本1
def target(): print('this is target') def decorator(func): func() print('this is decorator') decorator(target) ------------------------------------------- 運行結果為: this is target this is decorator

 

  Python允許將方法當作參數傳遞,因此以上腳本就是將target方法作為參數傳入decorator方法中,這其實也是裝飾器的工作原理,以上代碼等同於:

 

#腳本2
def decorator(func): func() print('this is decorator') @decorator def target(): print('this is target') target ------------------------------------------- 運行結果: this is target this is decorator

 

  因此可以看出,所謂的裝飾器就是利用了Python的方法可作參數傳遞的特性,將方法target作為參數傳遞到方法decorator中。

 

@decorator
def target():
    ...

 

  這種在一個方法的上方加一個@符號的寫法,就是表示位於下方的方法將被作為參數傳遞到位於@后面的decorator方法中。使用@符號只是讓腳本1中的代碼換了一個寫法,更加好看,當然也會更加靈活與實用,后面會講到這點。但它們的本質其實是一樣的,這也就是裝飾器的工作原理。

 

  2. 裝飾器的原理

 

  如果你仔細看的話,會在腳本2中發現一個問題,那就是腳本2中最后一行的target只是一個方法名字,它不是正確的方法調用,正確寫法應該加上左右括號的target(),如下:

 

#腳本3

def decorator(func):
    func()
    print('this is decorator')

@decorator
def target():
    print('this is target')

target()
--------------------------------------------
運行結果:

this is target
this is decorator
Traceback (most recent call last):
  File "C:/Users/Me/Desktop/ff.py", line 34, in <module>
    target()
TypeError: 'NoneType' object is not callable

 

   正如你所看到的,如果按照正確的寫法,運行結果你會看到應該出現的兩行打印文字"this is target"和"this is decorator",還會出現錯誤提示,ff.py是我為寫這篇心得臨時編寫的一個py腳本名字,提示說'NoneType'對象不可調用。這是怎么回事?好吧,我現在必須告訴你,其實腳本2和腳本3中並不是一個使用裝飾器的正確寫法,不是使用錯誤,而是作為裝飾器的decorator方法寫的並不友好,是的,我不認為它是錯誤的寫法,只是不友好。但只要你明白其中的道理,使用恰當的手段也是可以運行正常的,這就是為什么腳本2看似寫錯了調用方法卻得出了正確的結果。當然學習還是得規規矩矩,后面我會具體說正確的裝飾器怎么書寫,在這里我先解釋了一下腳本2和腳本3的運行原理,了解它們的運行原理和錯誤原因,其實就是了解裝飾器的原理。

  腳本2和腳本3的區別在於target和target(),也就是說真正的差別在於()這個括號。當()被附加在方法或者類后面時,表示調用,或者稱為運行及實例化,無論稱呼怎樣,本質意義沒有不同,都是調用給出的對象,當對象不具備調用性的時候,就會報錯:'某個類型' object is not callable。當一個方法被調用后,即target(),是否能被再次執行,取決於它是否會return一個對象,並且該對象可以被調用。也許你會有點迷糊,對比一下代碼會比較容易理解我想表達的意思:

 

 1 >>>def returnme():
 2 >>>    print('this is returnme')
 3  
 4 >>>def target():
 5 >>>    print('this is target')
 6   
 7 >>>target
 8 <function target at 0x00000000030A40D0>
 9   
10 >>>target()
11 target
12 <function returnme at 0x00000000030A4268>
13  
14 >>>target()()
15 target
16 returnme
17 
18 >>>returnme()()
19 returnme
20 Traceback (most recent call last):
21   File "<pyshell#15>", line 1, in <module>
22     returnme()()
23 TypeError: 'NoneType' object is not callable

 

  如上所示,當直接在腳本中輸入target,它只是告訴編譯器(我想是編譯器吧,因為我也不是很懂所謂編譯器的部分),總之就是告訴那個不知道在哪個角落控制着所有python代碼運行的“大腦”,在

0x00000000030A40D0位置(這個位置應該是指內存位置)存有一個function(方法)叫target;在target后面加上(),表示調用該方法,即輸入target(),“大腦”便按照target方法所寫的代碼逐條執行,於是打印出了target字符串,並且“大腦”明白在0x00000000030A4268位置有一個叫returnme的方法;因為target對象調用后是會返回一個returnme方法,並且方法是可以被調用的,因此你可以直接這樣書寫target()(),“大腦”會逐條執行target中的代碼,然后return一個returnme,因為多加了一個(),表示要對返回的returnme進行調用,於是再次逐條執行returnme中的代碼,最后便能看到15、16的打印結果;而returnme方法是沒有返回任何可調用的對象,因此當輸入returnme()()時,“大腦”會報錯。

  下面我們可以來解釋一下腳本2和腳本3的運行詳情,之前說過,裝飾器的工作原理就是腳本1代碼所演示的那樣。

 

@decorator
def target():
    ...
等同於
def decorator(target)(): ...

注:python語法中以上寫法是非法的,以上只是為了便於理解。

 

  當你調用被裝飾方法target時,其實首先被執行的是作為裝飾器的decorator函數,然后“大腦”會把target方法作為參數傳進去,於是:

 

#腳本2
def decorator(func):
    func()
    print('this is decorator')
  
@decorator
def target():
    print('this is target')
  
target
-------------------------------------------

實際運行情況:
首先調用decorator方法:decorator()
因為decorator方法含1個參數,因此將target傳入:decorator(target)
運行代碼“func()”,根據傳入的參數,實際執行target(),結果打印出:this is target
運行代碼"print('this is decorator')",結果打印出:this is decorator

 

   對比腳本3的運行情況:

 

#腳本3
def decorator(func):
    func()
    print('this is decorator')
  
@decorator
def target():
    print('this is target')
  
target()
-------------------------------------------

實際運行情況:
首先調用decorator方法:decorator()
因為decorator方法含1個參數,因此將target傳入:decorator(target)
運行代碼“func()”,根據傳入的參數,實際執行target(),結果打印出:this is target
運行代碼"print('this is decorator')",結果打印出:this is decorator

以上與腳本2中運行情況完全相同,接下來便是執行腳本2中target沒有的(),也就是執行調用命令。
由於decorator(target)沒有返回一個可以被調用的對象,因此“大腦”提示錯誤:'NoneType' object is not callable

 

  如果你還不是很清楚,請看下面的等價關系:

 

@decorator
def target():
    ...

等同於
def decorator(target)():
    ...

因此:
target == decorator(target)
target() == decorator(target)()

所以:
假設有一個變量var=target,在將target賦值給var時,其實是將decorator(target)的調用結果賦值給var,因為var不具備調用性(not callable),因此執行var()時,編譯器會報錯它是個NoneType對象,不能調用。

 

   綜上所述,你大概已經能夠明白所謂的裝飾器是怎么一回事,它是怎么工作的。但腳本2和腳本3中的寫法會帶來一些困惑,這個困惑就是通過我們編寫的decorator裝飾器對target進行裝飾后,將target變成了一個永遠不能被調用的方法,或者說變成了一個調用就報錯的方法。這跟我們的使用習慣以及對方法的認識是很不協調的,畢竟我們還是習慣一個方法天生注定可以被調用這種看法。所以為了滿足我們對方法的定義,我們最好將作為裝飾器的方法寫成一個可以返回具有被調用能力的對象的方法。

 

#腳本4
def whatever(): print('this is whatever') def decorator(func): func() print('this is decorator') return whatever  #1 @decorator def target(): print('this is target') ------------------------------ 輸入:target 結果: this is target this is decorator 輸入:target() 結果: this is target this is decorator this is whatever

 

  在#1的位置,你可以return任何可以被調用的方法或類,甚至你可以直接寫成:

 

def whatever():
    print('this is whatever')

def decorator(func):
    return whatever

@decorator
def target():
    print('this is target')

------------------------------
輸入:target
結果:告訴編譯器在內存某個位置有一個叫whatever的方法

輸入:target()
結果:this is whatever

 

  以上裝飾器的作用就是將target方法,完完全全變成了whatever方法。但這只能完美解釋裝飾器的功效,在實際當中則毫無意義,為什么要辛辛苦苦寫了一大堆代碼之后,最后的結果卻是完完整整地去調用另一個早已存在的方法?如果要調用whatever方法,我為什么不在我的代碼中直接寫whatever呢?

  裝飾器,顧名思義,它就是要對被裝飾的對象進行一些修改,通過再包裝來達到不一樣的效果,盡管它可以將被裝飾對象變得面目全非或者直接變成另一個對象,但這不是它被發明出來的主要意義,相反它被發明出來是幫助我們更高效更簡潔的編寫代碼,對其他方法或類進行裝飾,而非摧毀它們。例如對接收數據的檢校,我們只要寫好一個校驗方法,便可以在其他許多方法前作為裝飾器使用。

  

  3. 常規的裝飾器

 

  從廣義上來說,裝飾器就是腳本1中,利用python可以將方法傳參的特性,用一個方法去改變另一個方法,只要改變成功,都可以認為是合格的裝飾器。但這是理論上的合格,畢竟裝飾器是一種語法糖,應該是為我們帶來便利而不是無用功,所以:

 

1. 將方法裝飾成不能被調用,不好:
def decorator(func):
    func()
    print('this is decorator')

2. 將原方法徹底消滅,直接變成另一個方法,不好:
def decorator(func):
    return whatever

3. 保留原方法的同時再加上別的功能,不好:
def decorator(func):
    func()
    print('this is decorator')
    return whatever

 

  在以上3種寫法中,前兩種明顯不好,簡直就是將裝飾器變成了惡魔。而第3種寫法,雖然看起來重新包裝了被修飾方法,但卻在方法調用前擅自執行了一些命令,即當你輸入target,而非target()時:

 

#腳本4
def whatever():
    print('this is whatever')

def decorator(func):
    func()
    print('this is decorator')
    return whatever  #1

@decorator
def target():
    print('this is target')

------------------------------
輸入:target
結果:
this is target
this is decorator

 

  你尚未執行target(),編譯器卻已經打印了兩行字符串。這並不是我們想要的,當我們在代碼中寫下target時,我們是不希望編譯器立即執行什么命令,我們是希望編譯器在碰到target()時才執行運算。而且如果我們並不希望返回whatever,我們只想要通過裝飾器,使得target方法除了打印自己的"this is target",再多打印一行"this is decorator”,所有代碼只含有target和decorator兩個方法,無其他方法介入,應該怎么辦?

  當你這樣問的時候,其實就是已經開始了解裝飾器存在的意義了。以下便是為解決這些問題的裝飾器的常規寫法:

 

#腳本5

def decorator(func):
    def restructure():    
        func()
        print('this is decorator')
    return restructure

@decorator
def target():
    print('this is target')

 

  是的,從最外層講,以上代碼其實只有兩個方法,decorator和target——裝飾和被裝飾方法。但在decorator內部內嵌了一個方法restructure,這個內嵌的方法才是真正改造target的東西,而decorator其實只是負責將target傳入。這里的restructure,相當於在腳本4中,被寫在decorator外面的whatever角色,用裝飾器的常規寫法也可以寫出腳本4的效果,如下:

 

def decorator(func):   
    func()
    print('this is decorator')

    def whatever():
        print('this is whatever')     
    return restructure

@decorator
def target():
    print('this is target')

 

   對比以上的寫法你會發現,python的方法傳參和類的繼承性質很相似,在decorator之內,whatever之外,你可以寫入任何代碼,當執行target時,就開始初始化decorator,也就是執行decorator內部可以執行的代碼;當執行target()時,會對初始化之后的decorator開始調用,因此這就要求decorator完成初始化之后必須返回一個具備調用性的對象。所以,除非你想要target方法初始化之前(實際上是對decorator進行初始化)就執行一些代碼,否則不要在decorator和whatever中間插入代碼。

  正常情況下,當decorator完成初始化,應該return一個可調用對象,也就是腳本5中的restructure方法,這個方法就是替代target的克隆人,在restructure中你可以對target進行重寫,或其他代碼來包裝target。因此你只是想初始化target的話(實際就是對restructure初始化),就應將你要初始化的代碼寫入到restructure內部去。另外你也可以在decorator中內嵌多個方法,或多層方法,例如:

 

#腳本6

def decorator(func):
    def restructure():    
        func()
        print('this is decorator')

    def whatever():
        func()
        print('this is whatever')

    return restructure

@decorator
def target():
    print('this is target')

 

  被decorator裝飾的target最后會多打印一行'this is decorator'還是'this is whatever’,取決於decorator方法return的是它內部的哪一個方法(restructure或whatever),因此以上代碼等價於以下寫法:

 

執行target()

等同於

首先初始化decorator(target),結果返回一個restructure,即:target == decorator(target) == restructure。

然后調用,即:target() == decorator(target)() == restructure()

與類一樣,當target被傳入decorator之后,作為decorator內嵌方法是可以調用(繼承)target方法的,這就是為什么restructure不用接受傳參就可以改造target的原因。

注:在python中,以上多個()()的寫法是非法的,這樣寫只是為了便於理解。

 

  裝飾器這個概念本來也可以設計成多個()這樣的形式,但這樣就破壞了python的基本寫法,而且不好看,尤其當有多重裝飾的時候,因此使用@置於方法前的設計是更加優雅和清晰的。

  我之所以使用多個()這樣並不被python支持的寫法來闡述我對python的裝飾器的理解,是因為我相信通過這樣被python放棄的語法也能更好地幫助你理解python的繼承、傳參以及裝飾器,尤其是帶有參數的裝飾器。

 

  4. 裝飾帶有參數的方法

 

  首先請看以下代碼:

 

def target(x):
    print('this is target %s'%x)

def decorator(func,x):
    func(x)
    print('this is decorator %s'%x)

decorator(target,'!')

等同於:

def decorator(func):
    def restructure(x):
        func(x)
        print('this is decorator %s'%x)
    return restructure

@decorator
def target(x):
    print('this is target %s'%x)

target('!')

 

  target(x)中的參數x是如何傳入,什么時候傳入裝飾器的?首先嘗試以下代碼:

 

def decorator(func):
    print(x)  #增加一行代碼
    def restructure(x):
        func(x)
        print('this is decorator %s'%x)
    return restructure

@decorator
def target(x):
    print('this is target %s'%x)

target('!')

 

  此時編譯器會報錯參數x沒有被定義,也就是說在初始化decorator的時候,只有target方法被傳入,其參數x='!'並沒有傳入。

  現在讓我們回顧一下之前說的裝飾器工作原理,如下:

 

target == decorator(target) == restructure。

target() == decorator(target)() == restructure()

同理:

target(x) == decorator(target)(x) == restructure(x)

 

  所以,你可以很清楚地了解到,作為target的參數,其實不是傳給decorator,而是傳給初始化完decorator之后return的restructure。因此,如果裝飾器寫成如下代碼: 

 

def decorator(func): def restructure():    #不帶參數的方法
 func(x) print('this is decorator %s'%x) return restructure

 

  此時你輸入target('!'),編譯器會告訴你restructure沒有參數,但被傳入了1個參數,這個被傳入的參數就是x='!'。

  所以你現在明白了被裝飾的方法target(x),方法target和參數x是如何被傳入的,所以你必須保證初始化decorator之后返回的對象restructure方法形參與被裝飾的target方法形參相匹配,即:

 

如果定義為:def target(x) 則裝飾器中:def restructure(x) 如果定義為:def target(x,y) 則裝飾器中:def restructure(x,y)

 

  你也許會發現如果想裝飾器同時裝飾target(x)和newtarget(x,y),以上寫法就無法滿足要求了。因此為了讓裝飾器可以適用於更多的對象,我們最好讓裝飾器寫成如下形式:

 

def decorator(func): def restructure(*x): func(*x) print('this is decorator') return restructure @decorator def target(x): print('this is target %s'%x) @decorator def newtarget(x,y): print('this is target %s%s'%(x,y)) target('!') newtarget('!','?')

 

  利用python的帶星號參數語法(*arg),你便可以傳入任意數量的參數,你也可以設置帶雙星號的形參(**arg),便可以傳入字典形式的參數,單星形參和雙星形參可以同時使用,如:def restructure(*arg, **arg)。

 

  5. 帶有參數的裝飾器

 

  只要記住以上的裝飾器工作原理,你便可以知道如何寫出帶有參數的裝飾器,如:

 

def newdecorator(i): def decorator(func): def restructure(x): func(x) print('this is decorator %s%s'%(i,x)) return restructure return decorator @newdecorator('?') def target(x): print('this is target %s'%x) target('!') ------------------------------------------------------- 結果: this is target ! this is decorator ?!

 

  以上代碼實際上是:

 

target(x) == newdecorator(i)(target)(x) == decorator(target)(x) == reconstructure(x)

 

  同理,為了滿足不同數量的參數傳入,你也可以將newdecorator(i)寫成newdecorator(*i, **ii)。

 

  6. 結束語

 

  只要明白裝飾器的設計原理,便可以自如地寫出想要的裝飾器,哪怕是多重、多參數的裝飾器。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM