1. 程式人生 > >裝飾器編寫--要點

裝飾器編寫--要點

編寫裝飾器無外乎涉及到幾種引數-->裝飾器自己的引數,被裝飾函式,被裝飾函式的引數,正好對應三個引數入口(也即三個函式)來按順序接收這些引數;函式式中就是三層巢狀函式,類式裝飾器就是初始化方法,__call__方法,加一個__call__下巢狀的包裹方法即可!!

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/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)

@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)