歡迎光臨
每天分享高質量文章

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

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

作者:Toby
原文:https://betacat.online/posts/python-decorator/index.html

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 () 的基礎上做了一些封裝,因為 setterdeleterproperty () 的第二和第三個引數,getter 裝飾器和不帶 getter 的屬性裝飾器效果是一樣的,估計只是為了湊數,本身沒有任何存在的意義。經過 @ property 裝飾過的函式傳回的不再是一個函式,而是一個 property 物件。

>>> property()

@ 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 "{content}".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.
Hello Toby!< /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 ,那麼上面的這個陳述句剛好就是把這個結果賦值給 saysay__name__ 自然也就是 wrapper 了,不僅僅是 name ,其他屬性也都是來自 wrapper ,比如 docsource 等等。

使用標準庫裡的 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:\Python27\lib\functools.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 實現的裝飾器能完整保留原函式的 namedocargs ,唯一有問題的就是 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 的值你能夠更加靈活的調整你的裝飾器。另外, argskwargs 也是固定的,註意前面沒有星號。在裝飾器內部呼叫原函式時才帶星號。

如果你需要使用 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 的函式依次執行等等。

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

本文原始碼

轉載宣告:本文轉載自「程式設計派」,搜尋「codingpy」即可關註。

《Python人工智慧和全棧開發》2018年07月23日即將在北京開課,120天衝擊Python年薪30萬,改變速約~~~~

*宣告:推送內容及圖片來源於網路,部分內容會有所改動,版權歸原作者所有,如來源資訊有誤或侵犯權益,請聯絡我們刪除或授權事宜。


– END –


更多Python好文請點選【閱讀原文】哦

↓↓↓

贊(0)

分享創造快樂