當前位置:
首頁 > 知識 > 簡單地理解 Python 的裝飾器

簡單地理解 Python 的裝飾器

(點擊

上方藍字

,快速關注我們)




來源:0xFEE1C001


www.lightxue.com/understand-python-decorator-the-easy-way


如有好文章投稿,請點擊 → 這裡了解詳情




Python有大量強大又貼心的特性,如果要列個最受歡迎排行榜,那麼裝飾器絕對會在其中。



剛接觸裝飾器,會覺得代碼不多卻難以理解。其實裝飾器的語法本身挺簡單的,複雜是因為同時混雜了其它的概念。下面我們一起拋去無關概念,簡單地理解下Python的裝飾器。




裝飾器的原理




在解釋器下跑個裝飾器的例子,直觀地感受一下。





# make_bold就是裝飾器,實現方式這裡略去


>>>

@

make

_

bold


...

def get_content

()

:


...

    

return

"hello world"


...


>>>

get_content

()


"<b>hello world</b>"



被make_bold裝飾的get_content,調用後返回結果會自動被b標籤包住。怎麼做到的呢,簡單4步就能明白了。




1. 函數是對象




我們定義個get_content函數。這時get_content也是個對象,它能做所有對象的操作。





def get_content

()

:


    

return

"hello world"



它有id,有type,有值。





>>>

id

(

get_content

)


140090200473112


>>>

type

(

get_content

)


<

class

"function"

>


>>>

get_content


<

function

get_content

at

0x7f694aa2be18

>




跟其他對象一樣可以被賦值給其它變數。





>>>

func_name

=

get_content


>>>

func_name

()


"hello world"




它可以當參數傳遞,也可以當返回值





>>>

def foo

(

bar

)

:


...

    

print

(

bar

())


...

    

return

bar


...


>>>

func

=

foo

(

get_content

)


hello

world


>>>

func

()


"hello world"




2. 自定義函數對象




我們可以用class來構造函數對象。有成員函數__call__的就是函數對象了,函數對象被調用時正是調用的__call__。





class

FuncObj

(

object

)

:


    

def __init__

(

self

,

name

)

:


        

print

(

"Initialize"

)


        

self

.

name

=

name


 


    def __call__

(

self

)

:


        

print

(

"Hi"

,

self

.

name

)




我們來調用看看。可以看到,

函數對象的使用分兩步:構造和調用

(同學們注意了,這是考點)。





>>>

fo

=

FuncObj

(

"python"

)


Initialize


>>>

fo

()


Hi

python




3. @是個語法糖




裝飾器的@沒有做什麼特別的事,不用它也可以實現一樣的功能,只不過需要更多的代碼。





@

make_bold


def get_content

()

:


    

return

"hello world"


 


# 上面的代碼等價於下面的


 


def get_content

()

:


    

return

"hello world"


get_content

=

make_bold

(

get_content

)




make_bold是個函數,要求入參是函數對象,返回值是函數對象。@的語法糖其實是省去了上面最後一行代碼,使可讀性更好。用了裝飾器後,每次調用get_content,真正調用的是make_bold返回的函數對象。




4. 用類實現裝飾器




入參是函數對象,返回是函數對象,如果第2步里的類的構造函數改成入參是個函數對象,不就正好符合要求嗎?我們來試試實現make_bold。





class

make_bold

(

object

)

:


    

def __init__

(

self

,

func

)

:


        

print

(

"Initialize"

)


        

self

.

func

=

func


 


    def __call__

(

self

)

:


        

print

(

"Call"

)


        

return

"<b>{}</b>"

.

format

(

self

.

func

())




大功告成,看看能不能用。





>>>

@

make

_

bold


...

def get_content

()

:


...

    

return

"hello world"


...


Initialize


>>>

get_content

()


Call


"<b>hello world</b>"




成功實現裝飾器!是不是很簡單?




這裡分析一下之前強調的構造和調用兩個過程。我們去掉@語法糖好理解一些。





# 構造,使用裝飾器時構造函數對象,調用了__init__


>>>

get_content

=

make_bold

(

get_content

)


Initialize


 


# 調用,實際上直接調用的是make_bold構造出來的函數對象


>>>

get_content

()


Call


"<b>hello world</b>"




到這裡就徹底清楚了,完結撒花,可以關掉網頁了~~~(如果只是想知道裝飾器原理的話)




函數版裝飾器




閱讀源碼時,經常見到用嵌套函數實現的裝飾器,怎麼理解?同樣僅需4步。




1. def的函數對象初始化




用class實現的函數對象很容易看到什麼時候構造的,那def定義的函數對象什麼時候構造的呢?





# 這裡的全局變數刪去了無關的內容


>>>

globals

()


{}


>>>

def func

()

:


...

    

pass


...


>>>

globals

()


{

"func"

: <

function

func

at

0x10f5baf28

>

}




不像一些編譯型語言,程序在啟動時函數已經構造那好了。上面的例子可以看到,執行到def會才構造出一個函數對象,並賦值給變數make_bold。




這段代碼和下面的代碼效果是很像的。





class

NoName

(

object

)

:


    

def __call__

(

self

)

:


        

pass


 


func

=

NoName

()




2. 嵌套函數




Python的函數可以嵌套定義。





def outer

()

:


    

print

(

"Before def:"

,

locals

())


    

def inner

()

:


        

pass


    print

(

"After def:"

,

locals

())


    

return

inner




inner是在outer內定義的,所以算outer的局部變數。執行到def inner時函數對象才創建,因此每次調用outer都會創建一個新的inner。下面可以看出,每次返回的inner是不同的。





>>>

outer

()


Before

def

:

{}


After

def

:

{

"inner"

: <

function

outer

.

<

locals

>

.

inner

at

0x7f0b18fa0048

>

}


<

function

outer

.

<

locals

>

.

inner

at

0x7f0b18fa0048

>


>>>

outer

()


Before

def

:

{}


After

def

:

{

"inner"

: <

function

outer

.

<

locals

>

.

inner

at

0x7f0b18fa00d0

>

}


<

function

outer

.

<

locals

>

.

inner

at

0x7f0b18fa00d0

>




3. 閉包




嵌套函數有什麼特別之處?因為有閉包。





def outer

()

:


    

msg

=

"hello world"


    

def inner

()

:


        

print

(

msg

)


    

return

inner




下面的試驗表明,inner可以訪問到outer的局部變數msg。





>>>

func

=

outer

()


>>>

func

()


hello

world




閉包有2個特點






  1. inner能訪問outer及其祖先函數的命名空間內的變數(局部變數,函數參數)。



  2. 調用outer已經返回了,但是它的命名空間被返回的inner對象引用,所以還不會被回收。




這部分想深入可以去了解Python的LEGB規則。




4. 用函數實現裝飾器




裝飾器要求入參是函數對象,返回值是函數對象,嵌套函數完全能勝任。





def make_bold

(

func

)

:


    

print

(

"Initialize"

)


    

def wrapper

()

:


        

print

(

"Call"

)


        

return

"<b>{}</b>"

.

format

(

func

())


    

return

wrapper




用法跟類實現的裝飾器一樣。可以去掉@語法糖分析下構造和調用的時機。





>>>

@

make

_

bold


...

def get_content

()

:


...

    

return

"hello world"


...


Initialize


>>>

get_content

()


Call


"<b>hello world</b>"




因為返回的wrapper還在引用著,所以存在於make_bold命名空間的func不會消失。make_bold可以裝飾多個函數,wrapper不會調用混淆,因為每次調用make_bold,都會有創建新的命名空間和新的wrapper。




到此函數實現裝飾器也理清楚了,完結撒花,可以關掉網頁了~~~(後面是使用裝飾的常見問題)




常見問題




1. 怎麼實現帶參數的裝飾器?




帶參數的裝飾器,有時會異常的好用。我們看個例子。





>>>

@

make_header

(

2

)


...

def get_content

()

:


...

    

return

"hello world"


...


>>>

get_content

()


"<h2>hello world</h2>"




怎麼做到的呢?其實這跟裝飾器語法沒什麼關係。去掉@語法糖會變得很容易理解。





@

make_header

(

2

)


def get_content

()

:


    

return

"hello world"


 


# 等價於


 


def get_content

()

:


    

return

"hello world"


unnamed_decorator

=

make_header

(

2

)


get_content

=

unnamed_decorator

(

get_content

)




上面代碼中的unnamed_decorator才是真正的裝飾器,make_header是個普通的函數,它的返回值是裝飾器。




來看一下實現的代碼。





def make_header

(

level

)

:


    

print

(

"Create decorator"

)


 


    

# 這部分跟通常的裝飾器一樣,只是wrapper通過閉包訪問了變數level


    

def decorator

(

func

)

:


        

print

(

"Initialize"

)


        

def wrapper

()

:


            

print

(

"Call"

)


            

return

"<h{0}>{1}</h{0}>"

.

format

(

level

,

func

())


        

return

wrapper


 


    

# make_header返回裝飾器


    

return

decorator




看了實現代碼,裝飾器的構造和調用的時序已經很清楚了。





>>>

@

make_header

(

2

)


...

def get_content

()

:


...

    

return

"hello world"


...


Create decorator


Initialize


>>>

get_content

()


Call


"<h2>hello world</h2>"




2. 如何裝飾有參數的函數?




為了有條理地理解裝飾器,之前例子里的被裝飾函數有意設計成無參的。我們來看個例子。





@

make_bold


def get_login_tip

(

name

)

:


    

return

"Welcome back, {}"

.

format

(

name

)




最直接的想法是把get_login_tip的參數透傳下去。





class

make_bold

(

object

)

:


    

def __init__

(

self

,

func

)

:


        

self

.

func

=

func


 


    def __call__

(

self

,

name

)

:


        

return

"<b>{}</b>"

.

format

(

self

.

func

(

name

))




如果被裝飾的函數參數是明確固定的,這麼寫是沒有問題的。但是make_bold明顯不是這種場景。它既需要裝飾沒有參數的get_content,又需要裝飾有參數的get_login_tip。這時候就需要可變參數了。





class

make_bold

(

object

)

:


    

def __init__

(

self

,

func

)

:


        

self

.

func

=

func


    def __call__

(

self

,

*

args

,

**

kwargs

)

:


        

return

"<b>{}</b>"

.

format

(

self

.

func

(

*

args

,

**

kwargs

))




當裝飾器不關心被裝飾函數的參數,或是被裝飾函數的參數多種多樣的時候,可變參數非常合適。可變參數不屬於裝飾器的語法內容,這裡就不深入探討了。




3. 一個函數能否被多個裝飾器裝飾?




下面這麼寫合法嗎?





@

make

_

italic


@

make_bold


def get_content

()

:


    

return

"hello world"




合法。上面的的代碼和下面等價,留意一下裝飾的順序。





def get_content

()

:


    

return

"hello world"


get_content

=

make_bold

(

get_content

)

# 先裝飾離函數定義近的


get_content

=

make_italic

(

get_content

)




4. functools.wraps有什麼用?




Python的裝飾器倍感貼心的地方是對調用方透明。調用方完全不知道也不需要知道調用的函數被裝飾了。這樣我們就能在調用方的代碼完全不改動的前提下,給函數patch功能。




為了對調用方透明,裝飾器返回的對象要偽裝成被裝飾的函數。偽裝得越像,對調用方來說差異越小。有時光偽裝函數名和參數是不夠的,因為Python的函數對象有一些元信息調用方可能讀取了。為了連這些元信息也偽裝上,functools.wraps出場了。它能用於把被調用函數的__module__,__name__,__qualname__,__doc__,__annotations__賦值給裝飾器返回的函數對象。





import functools


 


def make_bold

(

func

)

:


    

@

functools

.

wraps

(

func

)


    

def wrapper

(

*

args

,

**

kwargs

)

:


        

return

"<b>{}</b>"

.

format

(

func

(

*

args

,

**

kwargs

))


    

return

wrapper




對比一下效果。





>>>

@

make

_

bold


...

def get_content

()

:


...

    

"""Return page content"""


...

    

return

"hello world"


 


# 不用functools.wraps的結果


>>>

get_content

.

__name_

_


"wrapper"


>>>

get_content

.

__doc__


>>>


 


# 用functools.wraps的結果


>>>

get_content

.

__name_

_


"get_content"


>>>

get_content

.

__doc_

_


"Return page content"




實現裝飾器時往往不知道調用方會怎麼用,所以養成好習慣加上functools.wraps吧。




這次是真?完結了,有疑問請留言,撒花吧~~~




看完本文有收穫?請轉

發分享給更多人


關注「P

ython開發者」,提升Python技能


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Python開發者 的精彩文章:

標籤傳播演算法(Label Propagation)及 Python 實現
用 Python 3 的 async / await 做非同步編程
為什麼 Python 增長如此之快?
讓你的 Python 代碼優雅又地道
一個 Reentrant Error 引發的對 Python 信號機制的探索和思考

TAG:Python開發者 |