詳解Python的裝飾器

2021-02-24 凡貓軟體測試

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

為什麼需要裝飾器

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

def say_hello():
   print "hello!"
   def say_goodbye():
   print "hello!"  # bug hereif __name__ == '__main__':
   say_hello()
   say_goodbye()

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

[DEBUG]: Enter say_hello()Hello!
[DEBUG]: 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 wrapperdef say_hello():
   print "hello!"say_hello = debug(say_hello)  # 添加功能並保持原函數名不變

上面的debug函數其實已經是一個裝飾器了,它對原函數做了包裝並返回了另外一個函數,額外添加了一些功能。因為這樣寫實在不太優雅,在後面版本的Python中支持了@語法糖,下面代碼等同於早期的寫法。

def debug(func):
   def wrapper():
       print "[DEBUG]: enter {}()".format(func.__name__)        return func()    return wrapper@debugdef say_hello():
   print "hello!"

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

def debug(func):
   def wrapper(something):  # 指定一毛一樣的參數
       print "[DEBUG]: enter {}()".format(func.__name__)        return func(something)    return wrapper  # 返回包裝過函數@debugdef 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  # 返回@debugdef say(something):
   print "hello {}!".format(something)

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

高級一點的裝飾器

帶參數的裝飾器和類裝飾器屬於進階的內容。在理解這些裝飾器之前,最好對函數的閉包和裝飾器的接口約定有一定了解。(參見https://betacat.online/posts/2016-10-23/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)# 如果沒有使用@語法,等同於# say = logging(level='INFO')(say)@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)@loggingdef 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._xdef setx(self, value):
   self._x = value    
def delx(self):
  del self._x# create a propertyx = property(getx, setx, delx, "I am doc for x property")

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

@propertydef x(self): ...# 等同於def x(self): ...
x = property(x)

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

>>> property()
<property object at 0x10ff07940>

@staticmethod,@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 functionbegin 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@loggingdef 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@loggingdef say(something):
   """say something"""
   print "say {}!".format(something)print say.__name__  # sayprint say.__doc__ # say something

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

import inspectprint inspect.getargspec(say)  # failedprint 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  # 在@staticmethod之前裝飾,OK
   def check_model_for(obj):
       pass

如何優化你的裝飾器

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

decorator.py

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

from decorator import decoratedef 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@decoratordef 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.decoratordef logging(wrapped, instance, args, kwargs):  # instance is must
   print "[DEBUG]: enter {}()".format(wrapped.__name__)    return wrapped(*args, **kwargs)@loggingdef 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的使用,建議查閱官方文檔,在此不在贅述。

小結

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

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

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

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

相關焦點

  • Python 的lru_cache裝飾器使用簡介
    這篇文章主要介紹了Python 的lru_cache 裝飾器使用簡介,幫助大家更好的理解和學習使用python,感興趣的朋友可以了解下目錄
  • Python編寫裝飾器,給任意函數計時
    r} in {run_time:.4f} secs") return value return wrapper_timer大家注意,這個裝飾器會被任何函數使用。其中的func(*args, **kwargs)中的func就是目標函數,args、kwargs是這個函數調用的參數。這就實現了一個裝飾器。
  • Python遞歸函數、閉包和裝飾器
    外部函數返回值必須是內嵌函數 通過這三點,就可以創建一個閉包,提別注意:Python裝飾器就是使用了閉包。decorators(專業提高篇)裝飾器的定義裝飾器是程序開發中經常會用到的一個功能,用來在原有的函數上增添新的代碼需求。
  • 詳解python三大器——迭代器、生成器、裝飾器
    在python中如果一個對象實現了 __iter__方法,我們就稱之為可迭代對象,可以查看set\list\tuple…等源碼內部均實現了__iter__方法如果一個對象未實現__iter__方法,但是對其使用for…in則會拋出TypeError: 『xxx』 object is not iterable    可以通過isinstance(obj
  • (長文收藏) 如何理解 Python 裝飾器?
    作者:錢魏Wayhttps://www.biaodianfu.com/python-decorator.html大家好,歡迎來到 Crossin的編程教室 !裝飾器的辦法,定義一個專門日誌記錄的裝飾器,對需要的函數進行裝飾。
  • Python裝飾器以及高級用法
    這個簡單的事實是使python裝飾器成為可能的原因。查看下面的代碼,看看你是否可以猜出標記為A,B,C和D的行會發生什麼。另一方面,如果你對更多的話題感興趣的話,你可能會發現:如裝飾類:python @add_class_functionality class MyClass: ...
  • Python中unittest用法實例
    ,所有test運行前運行一次④ tearDownClass():必須使用@classmethod裝飾器,所有test運行完後運行一次2.您可能感興趣的文章:python中如何打包用戶自定義模塊詳解Python yaml模塊python用Configobj模塊讀取配置文件Python
  • 使用python強大的裝飾器,監控數據自動化流程日誌
    裝飾器是python當中的進階用法,並且應用非常廣泛,但是在實際的工作過程中,我發現很多人常常因為覺得裝飾器很複雜,望而生畏。實際上從原理來講並不難。實際上,在python當中有更加優雅的寫法,就是要講的裝飾器,用裝飾器的寫法如下:這段代碼和之前的代碼具有相同的功能,並且非常的簡潔,@my_decorator就相當於前面的greet=my_decorator(greet)語句,這樣寫還有一個好處,就是如果其他函數也想用my_decorator裝飾,那只需要把@my_decorator加在函數的上面就可以了。
  • Selenium2+python自動化55-unittest之裝飾器(@classmethod)
    這就需要用到裝飾器(@classmethod)來解決了。 一、裝飾器1.用setUp與setUpClass區別setup():每個測試case運行前運行teardown():每個測試case運行完後執行setUpClass():必須使用@classmethod 裝飾器,所有case運行前只運行一次tearDownClass():必須使用@classmethod裝飾器,所有case
  • Python退避及重試裝飾器:backoff
    退避和重試的函數裝飾器此模塊提供函數裝飾器,該函數裝飾器可用於包裝函數,以便函數重試直到滿足某些條件為止。當訪問可能出現間歇性故障的不可靠資源(如網絡資源和外部API)時,它應該有用。一般來說,它還可以用於外部生成內容的動態輪詢資源。
  • Python 裝飾器填坑指南 | 最常見的報錯信息、原因和解決方案
    Python 裝飾器簡介裝飾器(Decorator)是 Python 非常實用的一個語法糖功能。裝飾器本質是一種返回值也是函數的函數,可以稱之為「函數的函數」。其目的是在不對現有函數進行修改的情況下,實現額外的功能。在 Python 中,裝飾器屬於純粹的「語法糖」,不使用也沒關係,但是使用的話能夠大大簡化代碼,使代碼更加簡潔易讀。
  • Python 入門之多個裝飾器執行順序
    裝飾器是 Python 用於封裝函數或代碼的工具,網上可以搜到很多文章可以學習,在這裡要討論的是多個裝飾器執行順序的一個迷思。大部分涉及多個裝飾器裝飾的函數調用順序時都會說明它們是自上而下的,比如下面這個例子:def decorator_a(func):    print 'Get in decorator_a'    def inner_a(*args, **kwargs):        print 'Get in inner_a'        return
  • 一文讀懂python裝飾器由來(二)
    --  Illustrations by Charlie Davis  --上一篇文章主要以一步一步演進的方式介紹了裝飾器的工作原理以及使用
  • 第57p,裝飾器魔法糖,多個裝飾器的使用
    大家好,我是楊數Tos,這是《從零基礎到大神》系列課程的第X篇文章,第三階段的課程:Python進階知識:Python進階知識:詳細講解Python中的函數(十)====> 函數的嵌套之裝飾器詳解(下篇)。
  • Python unittest單元測試框架的使用
    當然是有的,可以使用skip裝飾器。python單元測試unittest實例詳解Python中unittest用法實例Python+request+unittest實現接口測試框架集成實例python中如何打包用戶自定義模塊
  • property裝飾器
    先介紹@property轉載連結:https://zhuanlan.zhihu.com/p/64487092內容python的@property是python的一種裝飾器,是用來修飾方法的。我們可以使用@property裝飾器來創建只讀屬性,@property裝飾器會將方法轉換為相同名稱的只讀屬性,可以與所定義的屬性配合使用,這樣可以防止屬性被修改。使用場景:1.修飾方法,是方法可以像屬性一樣訪問。
  • 一文讀懂Python裝飾器
    打開APP 一文讀懂Python裝飾器 工程師3 發表於 2018-04-28 10:48:00 談裝飾器前,還要先要明白一件事,Python 中的函數和 Java、C++不太一樣,Python 中的函數可以像普通變量一樣當做參數傳遞給另外一個函數,例如:
  • Python通過fnmatch模塊實現文件名匹配
    您可能感興趣的文章:python用Configobj模塊讀取配置文件Python中fnmatch模塊的使用詳情Python unittest裝飾器實現原理及代碼>對python的unittest架構公共參數token提取方法詳解Python單元測試工具doctest和unittest詳細使用解析Python + Requests + Unittest接口自動化測試實例分析python
  • Python with關鍵字原理詳解
    普通版:file = "test.txt"def fun1():    """    普通版    """    f = open(file, "w")    f.write("hello python")    f.close
  • Python 測試框架unittest和pytest的優劣
    這篇文章主要介紹了Python 測試框架unittest和pytest的優劣,幫助大家更好的進行python程序的測試,感興趣的朋友可以了解下