詳解Python的裝飾器

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

爲何須要裝飾器

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

def say_hello():
    print "hello!"
    
def say_goodbye():
    print "hello!"  # bug here

if __name__ == '__main__':
    say_hello()
    say_goodbye()

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

[DEBUG]: Enter say_hello()
Hello!
[DEBUG]: Enter say_goodbye()
Goodbye!

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

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相關的才須要呢?閉包

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

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

歸納的講,裝飾器的做用就是爲已經存在的函數或對象添加額外的功能post

怎麼寫一個裝飾器

在早些時候 (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函數其實已是一個裝飾器了,它對原函數作了包裝並返回了另一個函數,額外添加了一些功能。由於這樣寫實在不太優雅,在後面版本的Python中支持了@語法糖,下面代碼等同於早期的寫法。

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/p...

帶參數的裝飾器

假設咱們前文的裝飾器須要完成的功能不只僅是能在進入某個函數後打出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)
@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()
<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 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,那麼上面的這個語句恰好就是把這個結果賦值給saysay__name__天然也就是wrapper了,不只僅是name,其餘屬性也都是來自wrapper,好比docsource等等。

使用標準庫裏的functools.wraps,能夠基本解決這個問題。

from functools import wraps

def 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  # 在@staticmethod以前裝飾,OK
    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的使用,建議查閱官方文檔,在此不在贅述。

小結

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

裝飾器的理念是對原函數、對象的增強,至關於從新封裝,因此通常裝飾器函數都被命名爲wrapper(),意義在於包裝。函數只有在被調用時纔會發揮其做用。好比@logging裝飾器能夠在函數執行時額外輸出日誌,@cache裝飾過的函數能夠緩存計算結果等等。

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

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

本文源碼 https://github.com/tobyqin/py...

關於做者:Python技術愛好者,目前從事測試開發相關工做,轉載請註明原文出處。

歡迎關注個人博客 http://betacat.online,你能夠到個人公衆號中去當吃瓜羣衆。

Betacat.online

相關文章
相關標籤/搜索