當前位置:
首頁 > 知識 > 跨過這道裝飾器的坎,或許你就升級了

跨過這道裝飾器的坎,或許你就升級了


Linux編程

點擊右側關注,免費入門到精通!

作者丨Toby


https://betacat.online/posts/python-decorator/index.html

裝飾器是一個非常重要的 Python 概念,可能算是進階的一大門檻。本文比較全面地對裝飾器進行了介紹,並且配備了詳細的代碼示例,推薦閱讀。

Python 中的裝飾器是你進入 Python 大門的一道坎,不管你跨不跨過去它都在那裡。


為什麼需要裝飾器

我們假設你的程序實現了 say_hello () 和 say_goodbye () 兩個函數。


def

 

say_hello

()

:


   

print

 

"hello!"

def

 

say_goodbye

()

:


   

print

 

"hello!"

  

# bug here

if

 __name__ == 

"__main__"

:
   say_hello()
   say_goodbye()



但是在實際調用中,我們發現程序出錯了,上面的代碼列印了兩個 hello 。經過調試你發現是 say_goodbye () 出錯了。老闆要求調用每個方法前都要記錄進入函數的時間和名稱,比如這樣:


[DEBUG]

 2016 

-

 10 

-

 27 11

:11

:11

 

-

 

Enter

 

say_hello

()

Hello

!

[DEBUG]

 2016 

-

 10 

-

 27 11

:11

:11

 

-

 

Enter

 

say_goodbye

()

Goodbye

!



好,小 A 是個畢業生,他是這樣實現的。


def

 

say_hello

()

:

   

print

 

"[DEBUG]: enter say_hello()"


   

print

 

"hello!"

def

 

say_goodbye

()

:


   

print

 

"[DEBUG]: enter say_goodbye()"

   

print

 

"hello!"

if

 __name__ == 

"__main__"

:
   say_hello()
   say_goodbye()



很 low 吧? 嗯是的。小 B 工作有一段時間了,他告訴小 A 應該這樣寫。


def

 

debug

()

:


   

import

 inspect
   caller_name = inspect.stack()[

1

][

3

]    
   

print

 

"[DEBUG]: enter {}()"

.format(caller_name)

def

 

say_hello

()

:


   debug()    
   

print

 

"hello!"

def

 

say_goodbye

()

:


   debug()    
   

print

 

"goodbye!"

if

 __name__ == 

"__main__"

:
   say_hello()
   say_goodbye()



是不是好一點?那當然,但是每個業務函數里都要調用一下 debug () 函數,是不是很難受?萬一老闆說 say 相關的函數不用 debug , do 相關的才需要呢?



那麼裝飾器這時候應該登場了。




裝飾器本質上是一個 Python 函數,它可以讓其他函數在不需要做任何代碼變動的前提下增加額外功能,裝飾器的返回值也是一個函數對象。它經常用於有切面需求的場景,比如:插入日誌、性能測試、事務處理、緩存、許可權校驗等場景。裝飾器是解決這類問題的絕佳設計,有了裝飾器,我們就可以抽離出大量與函數功能本身無關的雷同代碼並繼續重用。



概括的講,裝飾器的作用就是為已經存在的函數或對象添加額外的功能。



怎麼寫一個裝飾器



在早些時候 ( Python Version < 2.4,2004年以前 ),為一個函數添加額外功能的寫法是這樣的。


def

 

debug

(func)

:


   

def

 

wrapper

()

:


       

print

 

"[DEBUG]: enter {}()"

.format(func.__name__)        
       

return

 func()    
   

return

 wrapper

def

 

say_hello

()

:


   

print

 

"hello!"

say_hello = debug(say_hello)  

# 添加功能並保持原函數名不變




上面的 debug 函數其實已經是一個裝飾器了,它對原函數做了包裝並返回了另外一個函數,額外添加了一些功能。因為這樣寫實在不太優雅, [email



protected],下面代碼等同於早期的寫法。


def

 

debug

(func)

:


   

def

 

wrapper

()

:


       

print

 

"[DEBUG]: enter {}()"

.format(func.__name__)        
       

return

 func()    
   

return

 wrapper

@debug


def

 

say_hello

()

:


   

print

 

"hello!"




這是最簡單的裝飾器,但是有一個問題,如果被裝飾的函數需要傳入參數,那麼這個裝飾器就壞了。因為返回的函數並不能接受參數,你可以指定裝飾器函數 wrapper 接受和原函數一樣的參數,比如:


def

 

debug

(func)

:


   

def

 

wrapper

(something)

:

  

# 指定一毛一樣的參數


       

print

 

"[DEBUG]: enter {}()"

.format(func.__name__)        
       

return

 func(something)    
   

return

 wrapper  

# 返回包裝過函數

@debug


def

 

say

(something)

:


   

print

 

"hello {}!"

.format(something)



這樣你就解決了一個問題,但又多了 N 個問題。因為函數有千千萬,你只管你自己的函數,別人的函數參數是什麼樣子,鬼知道?還好 Python 提供了可變參數 * args 和關鍵字參數 ** kwargs ,有了這兩個參數,裝飾器就可以用於任意目標函數了。


def

 

debug

(func)

:


   

def

 

wrapper

(*args, **kwargs)

:

  

# 指定宇宙無敵參數


       

print

 

"[DEBUG]: enter {}()"

.format(func.__name__)        
       

print

 

"Prepare and say..."

,        
       

return

 func(*args, **kwargs)    
   

return

 wrapper  

# 返回

@debug


def

 

say

(something)

:


   

print

 

"hello {}!"

.format(something)



至此,你已完全掌握初級的裝飾器寫法。



高級一點的裝飾器



帶參數的裝飾器和類裝飾器屬於進階的內容。在理解這些裝飾器之前,最好對函數的閉包和裝飾器的介面約定有一定了解。(參見http://betacat.online/posts/python- closure/)



帶參數的裝飾器



假設我們前文的裝飾器需要完成的功能不僅僅是能在進入某個函數後打出 log 信息,而且還需指定 log 的級別,那麼裝飾器就會是這樣的。


def

 

logging

(level)

:


   

def

 

wrapper

(func)

:


       

def

 

inner_wrapper

(*args, **kwargs)

:


           

print

 

"[{level}]: enter function {func}()"

.format(
               level=level,
               func=func.__name__)            
           

return

 func(*args, **kwargs)        
       

return

 inner_wrapper    
   

return

 wrapper

@logging(level="INFO")


def

 

say

(something)

:


   

print

 

"say {}!"

.format(something)

@logging(level="DEBUG")


def

 

do

(something)

:


   

print

 

"do {}..."

.format(something)

if

 __name__ == 

"__main__"

:
   say(

"hello"

)
   do(

"my work"

)



是不是有一些暈?你可以這麼理解,當帶參數的裝飾器被打在某個函數上時,比如 @ logging ( level =" DEBUG ") ,它其實是一個函數,會馬上被執行,只要這個它返回的結果是一個裝飾器時,那就沒問題。細細再體會一下。



基於類實現的裝飾器



裝飾器函數其實是這樣一個介面約束,它必須接受一個 callable 對象作為參數,然後返回一個 callable 對象。在 Python 中一般 callable 對象都是函數,但也有例外。只要某個對象重載了 __call__ () 方法,那麼這個對象就是 callable 的。


class

 

Test

()

:


   

def

 

__call__

(self)

:


       

print

 

"call me!"

t = Test()
t()  

# call me




像 __call__ 這樣前後都帶下劃線的方法在 Python 中被稱為內置方法,有時候也被稱為魔法方法。重載這些魔法方法一般會改變對象的內部行為。上面這個例子就讓一個類對象擁有了被調用的行為。



回到裝飾器上的概念上來,裝飾器要求接受一個 callable 對象,並返回一個 callable 對象(不太嚴謹,詳見後文)。那麼用類來實現也是也可以的。我們可以讓類的構造函數 __init__ () 接受一個函數,然後重載 __call__ () 並返回一個函數,也可以達到裝飾器函數的效果。


class

 

logging

(

object

):


   

def

 

__init__

(

self

, func)

:
       

self

.func = func    
   

def

 

__call__

(

self

, *args, **kwargs)

:
       print 

"[DEBUG]: enter function {func}()"

.format(
           func=

self

.func.__name_

_

)        
       

return

 

self

.func(*args, **kwargs)

@logging

def

 

say

(something)

:
   print 

"say {}!"

.format(something)



帶參數的類裝飾器



如果需要通過類形式實現帶參數的裝飾器,那麼會比前面的例子稍微複雜一點。那麼在構造函數里接受的就不是一個函數,而是傳入的參數。通過類把這些參數保存起來。然後在重載 __call__ 方法是就需要接受一個函數並返回一個函數。


class

 

logging

(

object

):


   

def

 

__init__

(

self

, level=

"INFO"

)

:
       

self

.level = level    
   

def

 

__call__

(

self

, func)

:  

# 接受函數


       

def

 

wrapper

(*args, **kwargs)

:
           print 

"[{level}]: enter function {func}()"

.format(
               level=

self

.level,
               func=func.__name_

_

)
           func(*args, **kwargs)        
           

return

 wrapper  

# 返回函數

@logging(level=

"INFO"

)

def

 

say

(something)

:
   print 

"say {}!"

.format(something)



內置的裝飾器



內置的裝飾器和普通的裝飾器原理是一樣的,只不過返回的不是函數,而是類對象,所以更難理解一些。



@ property



在了解這個裝飾器前,你需要知道在不使用裝飾器怎麼寫一個屬性。


def

 

getx

(

self

)

:
   

return

 

self

._x

def

 

setx

(

self

, value)

:
   

self

._x = value

def

 

delx

(

self

)

:
   del 

self

._x

# create a property


x = property(getx, setx, delx, 

"I am doc for x property"

)



以上就是一個Python屬性的標準寫法,其實和Java挺像的,能達到一樣的效果但看起來更簡單。


@property

def

 

x

(

self

)

: ...

# 等同於


def

 

x

(

self

)

: ...
x = property(x)



屬性有三個裝飾器: setter , getter , deleter



,都是在 property () 的基礎上做了一些封裝,因為 setter 和 deleter 是 property () 的第二和第三個參數,getter 裝飾器和不帶 getter 的屬性裝飾器效果是一樣的,估計只是為了湊數,本身沒有任何存在的意義。經過 @ property 裝飾過的函數返回的不再是一個函數,而是一個 property 對象。


>>> 

property

()
<

property

 

object

 at 

0

x10ff07940 >



@ classmethod



有了 @ property 裝飾器的了解,這兩個裝飾器的原理是差不多的。 @ staticmethod 返回的是一個 staticmethod 類對象,而 @ classmethod 返回的是一個 classmethod 類對象。他們都是調用的是各自的 __init__ () 構造函數。


class

 

classmethod

(object)

:


   

"""
   classmethod(function) -> method
   """


   

def

 

__init__

(self, function)

:

  

# for @classmethod decorator


       

pass


   

# ...


class

 

staticmethod

(object)

:


   

"""
   staticmethod(function) -> method
   """


   

def

 

__init__

(self, function)

:

  

# for @staticmethod decorator


       

pass


   

# ...


class

 

Foo

(object)

:


   @staticmethod
   

def

 

bar

()

:


       

pass


   

# 等同於 bar = staticmethod(bar)




至此,我們上文提到的裝飾器介面定義可以更加明確一些,裝飾器必須接受一個 callable 對象,其實它並不關心你返回什麼,可以是另外一個 callable 對象(大部分情況),也可以是其他類對象,比如 property 。



裝飾器里的那些坑



裝飾器可以讓你代碼更加優雅,減少重複,但也不全是優點,也會帶來一些問題。



位置錯誤的代碼



讓我們直接看示例代碼。


def

 

html_tags

(tag_name)

:


   

print

 

"begin outer function."


   

def

 

wrapper_

(func)

:


       

print

 

"begin of inner wrapper function."


       

def

 

wrapper

(*args, **kwargs)

:


           content = func(*args, **kwargs)            
           

print

 

"<{tag}>{content}</{tag}>"

.format(tag=tag_name, content=content)               

print

 

"end of inner wrapper function."


       

return

 wrapper    
   

print

 

"end of outer function"


   

return

 wrapper_@html_tags(

"b"

)

def

 

hello

(name=

"Toby"

)

:


   

return

 

"Hello {}!"

.format(name)
hello()
hello()



在裝飾器中我在各個可能的位置都加上了 print 語句,用於記錄被調用的情況。你知道他們最後列印出來的順序嗎?如果你心裡沒底,那麼最好不要在裝飾器函數之外添加邏輯功能,否則這個裝飾器就不受你控制了。以下是輸出結果:


begin

 

outer

 function.

end

 

of

 

outer

 

function


begin

 

of

 

inner

 wrapper function.

end

 

of

 

inner

 wrapper function.
<b > Hello Toby!< /b >
<b > Hello Toby!< /b >



錯誤的函數簽名和文檔



裝飾器裝飾過的函數看上去名字沒變,其實已經變了。


def

 

logging

(func)

:


   

def

 

wrapper

(*args, **kwargs)

:


       

"""print log before a function."""


       

print

 

"[DEBUG] {}: enter {}()"

.format(datetime.now(), func.__name__)        
       

return

 func(*args, **kwargs)    
   

return

 wrapper

@logging


def

 

say

(something)

:


   

"""say something"""


   

print

 

"say {}!"

.format(something)

print

 say.__name__  

# wrapper




為什麼會這樣呢?@等同於這樣的寫法。


say

 = logging(say)



logging 其實返回的函數名字剛好是 wrapper ,那麼上面的這個語句剛好就是把這個結果賦值給 say , say 的 __name__ 自然也就是 wrapper 了,不僅僅是 name ,其他屬性也都是來自 wrapper ,比如 doc , source 等等。



使用標準庫里的 functools.wraps ,可以基本解決這個問題。


from

 functools 

import

 wrapsdef logging(func):
   @wraps(func)
   

def

 

wrapper

(*args, **kwargs)

:


       

"""print log before a function."""


       

print

 

"[DEBUG] {}: enter {}()"

.format(datetime.now(), func.__name__)        
       

return

 func(*args, **kwargs)    
   

return

 wrapper

@logging


def

 

say

(something)

:


   

"""say something"""

   

print

 

"say {}!"

.format(something)
   

print

 say.__name__  

# say


   

print

 say.__doc__  

# say something




看上去不錯!主要問題解決了,但其實還不太完美。因為函數的簽名和源碼還是拿不到的。


import inspect

print

 inspect.getargspec(

say

)  

# failed


print

 inspect.getsource(

say

)  

# failed




如果要徹底解決這個問題可以借用第三方包,比如 wrapt 。後文有介紹。



不能裝飾@staticmethod 或者 @classmethod」



當你想把裝飾器用在一個靜態方法或者類方法時,不好意思,報錯了。


class

 

Car

(object)

:

   

def

 

__init__

(self, model)

:


       self.model = model    

   @logging  

# 裝飾實例方法,OK


   

def

 

run

(self)

:


       

print

 

"{} is running!"

.format(self.model)    

   @logging  

# 裝飾靜態方法,Failed


   @staticmethod
   

def

 

check_model_for

(obj)

:


       

if

 isinstance(obj, Car):            
           

print

 

"The model of your car is {}"

.format(obj.model)        
       

else

:            
           

print

 

"{} is not a car!"

.format(obj)     

"""
Traceback (most recent call last):
...
 File "example_4.py", line 10, in logging

   @wraps(func)
 File "C:Python27libfunctools.py", line 33, in update_wrapper

   setattr(wrapper, attr, getattr(wrapped, attr))
AttributeError: "staticmethod" object has no attribute "__module__"

"""




前面已經解釋了 @ staticmethod 這個裝飾器,其實它返回的並不是一個 callable 對象,而是一個 staticmethod 對象,那麼它是不符合裝飾器要求的(比如傳入一個 callable 對象),你自然不能在它之上再加別的裝飾器。要解決這個問題很簡單,只要把你的裝飾器放在 @ staticmethod 之前就好了,因為你的裝飾器返回的還是一個正常的函數,然後再加上一個 @ staticmethod 是不會出問題的。


class

 

Car

(

object

):


   

def

 

__init__

(

self

, model)

:
       

self

.model = model    

   @staticmethod
   @logging  
   

def

 

check_model_for

(obj)

:
       pass
       



如何優化你的裝飾器



嵌套的裝飾函數不太直觀,我們可以使用第三方包類改進這樣的情況,讓裝飾器函數可讀性更好。



decorator.py



decorator.py是一個非常簡單的裝飾器加強包。你可以很直觀的先定義包裝函數 wrapper () ,再使用 decorate ( func , wrapper ) 方法就可以完成一個裝飾器。


from

 decorator 

import

 decorate

def

 

wrapper

(func, *args, **kwargs)

:


   

"""print log before a function."""


   

print

 

"[DEBUG] {}: enter {}()"

.format(datetime.now(), func.__name__)    
   

return

 func(*args, **kwargs)

def

 

logging

(func)

:


   

return

 decorate(func, wrapper)  

# 用wrapper裝飾func




你也可以使用它自帶的 @ decorator 裝飾器來完成你的裝飾器。


from

 decorator 

import

 decorator

@decorator


def

 

logging

(func, *args, **kwargs)

:


   

print

 

"[DEBUG] {}: enter {}()"

.format(datetime.now(), func.__name__)    
   

return

 func(*args, **kwargs)



decorator.py 實現的裝飾器能完整保留原函數的 name , doc 和 args ,唯一有問題的就是 inspect.getsource ( func ) 返回的還是裝飾器的源代碼,你需要改成 inspect.getsource ( func.__wrapped__ ) 。



wrapt



wrapt是一個功能非常完善的包,用於實現各種你想到或者你沒想到的裝飾器。使用 wrapt 實現的裝飾器你不需要擔心之前 inspect 中遇到的所有問題,因為它都幫你處理了,甚至 inspect.getsource ( func ) 也準確無誤。


import

 wrapt

# without argument in decorator

@wrapt.decorator


def

 

logging

(wrapped, instance, args, kwargs)

:

  

# instance is must


   

print

 

"[DEBUG]: enter {}()"

.format(wrapped.__name__)    
   

return

 wrapped(*args, **kwargs)

@logging


def

 

say

(something)

:

 

pass




使用 wrapt 你只需要定義一個裝飾器函數,但是函數簽名是固定的,必須是 ( wrapped , instance, args, kwargs ) ,注意第二個參數 instance 是必須的,就算你不用它。當裝飾器裝飾在不同位置時它將得到不同的值,比如裝飾在類實例方法時你可以拿到這個類實例。根據 instance 的值你能夠更加靈活的調整你的裝飾器。另外, args 和 kwargs 也是固定的,注意前面沒有星號。在裝飾器內部調用原函數時才帶星號。



如果你需要使用 wrapt 寫一個帶參數的裝飾器,可以這樣寫。


def

 

logging

(level)

:


   @wrapt.decorator
   

def

 

wrapper

(wrapped, instance, args, kwargs)

:


       

print

 

"[{}]: enter {}()"

.format(level, wrapped.__name__)        
       

return

 wrapped(*args, **kwargs)    
   

return

 wrapper@logging(level=

"INFO"

)

def

 

do

(work)

:

 

pass




關於 wrapt 的使用,建議查閱官方文檔,在此不在贅述。



http://wrapt.readthedocs.io/en/latest/quick-start.html



小結



Python 的裝飾器和 Java 的註解( Annotation )並不是同一回事,和 C# 中的特性( Attribute )也不一樣,完全是兩個概念。



裝飾器的理念是對原函數、對象的加強,相當於重新封裝,所以一般裝飾器函數都被命名為 wrapper () ,意義在於包裝。函數只有在被調用時才會發揮其作用。比如 @ logging 裝飾器可以在函數執行時額外輸出日誌, @ cache 裝飾過的函數可以緩存計算結果等等。



而註解和特性則是對目標函數或對象添加一些屬性,相當於將其分類。這些屬性可以通過反射拿到,在程序運行時對不同的特性函數或對象加以干預。比如帶有 Setup 的函數就當成準備步驟執行,或者找到所有帶有 TestMethod 的函數依次執行等等。



至此我所了解的裝飾器已經講完,但是還有一些內容沒有提到,比如裝飾類的裝飾器。有機會再補充。謝謝觀看。




本文源碼 <

https://github.com/tobyqin/python_decorator

 推薦↓↓↓ 






??

16個技術公眾號

】都在這裡!


涵蓋:程序員大咖、源碼共讀、程序員共讀、數據結構與演算法、黑客技術和網路安全、大數據科技、編程前端、Java、Python、Web編程開發、Android、iOS開發、Linux、資料庫研發、幽默程序員等。

萬水千山總是情,點個 「

好看

」 行不行

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

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


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

愛情來的太快,根本擋不住!
這些函數簡直是屌爆了

TAG:Python開發 |