我們知道,在 Python 中,我們可以像使用變量一樣使用函數(shù):
簡而言之,函數(shù)就是一個對象。
為了更好地理解裝飾器,我們先從一個簡單的例子開始,假設有下面的函數(shù):
def hello():
return 'hello world'
現(xiàn)在我們想增強 hello() 函數(shù)的功能,希望給返回加上 HTML 標簽,比如 <i>hello world</i>,但是有一個要求,不改變原來 hello() 函數(shù)的定義。這里當然有很多種方法,下面給出一種跟本文相關的方法:
def makeitalic(func):
def wrapped():
return "<i>" + func() + "</i>"
return wrapped
在上面的代碼中,我們定義了一個函數(shù) makeitalic,該函數(shù)有一個參數(shù) func,它是一個函數(shù);在 makeitalic 函數(shù)里面我們又定義了一個內部函數(shù) wrapped,并將該函數(shù)作為返回。
現(xiàn)在,我們就可以不改變 hello() 函數(shù)的定義,給返回加上 HTML 標簽了:
>>> hello = makeitalic(hello) # 將 hello 函數(shù)傳給 makeitalic
>>> hello()
'<i>hello world</i>'
在上面,我們將 hello 函數(shù)傳給 makeitalic,再將返回賦給 hello,此時調用 hello() 就得到了我們想要的結果。
不過要注意的是,由于我們將 makeitalic 的返回賦給了 hello,此時 hello() 函數(shù)仍然存在,但是它的函數(shù)名不再是 hello 了,而是 wrapped,正是 makeitalic 返回函數(shù)的名稱,可以驗證一下:
>>> hello.__name__
'wrapped'
對于這個小瑕疵,后文將會給出解決方法。
現(xiàn)在,我們梳理一下上面的例子,為了增強原函數(shù) hello 的功能,我們定義了一個函數(shù),它接收原函數(shù)作為參數(shù),并返回一個新的函數(shù),完整的代碼如下:
def makeitalic(func):
def wrapped():
return "<i>" + func() + "</i>"
return wrapped
def hello():
return 'hello world'
hello = makeitalic(hello)
事實上,makeitalic 就是一個裝飾器(decorator),它『裝飾』了函數(shù) hello,并返回一個函數(shù),將其賦給 hello。
一般情況下,我們使用裝飾器提供的 @ 語法糖(Syntactic Sugar),來簡化上面的寫法:
def makeitalic(func):
def wrapped():
return "<i>" + func() + "</i>"
return wrapped
@makeitalic
def hello():
return 'hello world'
像上面的情況,可以動態(tài)修改函數(shù)(或類)功能的函數(shù)就是裝飾器。本質上,它是一個高階函數(shù),以被裝飾的函數(shù)(比如上面的 hello)為參數(shù),并返回一個包裝后的函數(shù)(比如上面的 wrapped)給被裝飾函數(shù)(hello)。
@decorator
def func():
pass
等價于下面的形式:
def func():
pass
func = decorator(func)
@decorator_one
@decorator_two
def func():
pass
等價于:
def func():
pass
func = decorator_one(decorator_two(func))
@decorator(arg1, arg2)
def func():
pass
等價于:
def func():
pass
func = decorator(arg1, arg2)(func)
下面我們再看一些具體的例子,以加深對它的理解。
前面的例子中,被裝飾的函數(shù) hello() 是沒有帶參數(shù)的,我們看看被裝飾函數(shù)帶參數(shù)的情況。對前面例子中的 hello() 函數(shù)進行改寫,使其帶參數(shù),如下:
def makeitalic(func):
def wrapped(*args, **kwargs):
ret = func(*args, **kwargs)
return '<i>' + ret + '</i>'
return wrapped
@makeitalic
def hello(name):
return 'hello %s' % name
@makeitalic
def hello2(name1, name2):
return 'hello %s, %s' % (name1, name2)
由于函數(shù) hello 帶參數(shù),因此內嵌包裝函數(shù) wrapped 也做了一點改變:
func,即被裝飾函數(shù),也就是說內嵌包裝函數(shù)的參數(shù)跟被裝飾函數(shù)的參數(shù)對應,這里使用了 (*args, **kwargs),是為了適應可變參數(shù)。看看使用:
>>> hello('python')
'<i>hello python</i>'
>>> hello2('python', 'java')
'<i>hello python, java</i>'
上面的例子,我們增強了函數(shù) hello 的功能,給它的返回加上了標簽 <i>...</i>,現(xiàn)在,我們想改用標簽 <b>...</b> 或 <p>...</p>。是不是要像前面一樣,再定義一個類似 makeitalic 的裝飾器呢?其實,我們可以定義一個函數(shù),將標簽作為參數(shù),返回一個裝飾器,比如:
def wrap_in_tag(tag):
def decorator(func):
def wrapped(*args, **kwargs):
ret = func(*args, **kwargs)
return '<' + tag + '>' + ret + '</' + tag + '>'
return wrapped
return decorator
現(xiàn)在,我們可以根據需要生成想要的裝飾器了:
makebold = wrap_in_tag('b') # 根據 'b' 返回 makebold 生成器
@makebold
def hello(name):
return 'hello %s' % name
>>> hello('world')
'<b>hello world</b>'
上面的形式也可以寫得更加簡潔:
@wrap_in_tag('b')
def hello(name):
return 'hello %s' % name
這就是帶參數(shù)的裝飾器,其實就是在裝飾器外面多了一層包裝,根據不同的參數(shù)返回不同的裝飾器。
現(xiàn)在,讓我們來看看多個裝飾器的例子,為了簡單起見,下面的例子就不使用帶參數(shù)的裝飾器。
def makebold(func):
def wrapped():
return '<b>' + func() + '</b>'
return wrapped
def makeitalic(func):
def wrapped():
return '<i>' + func() + '</i>'
return wrapped
@makebold
@makeitalic
def hello():
return 'hello world'
上面定義了兩個裝飾器,對 hello 進行裝飾,上面的最后幾行代碼相當于:
def hello():
return 'hello world'
hello = makebold(makeitalic(hello))
調用函數(shù) hello:
>>> hello()
'<b><i>hello world</i></b>'
前面的裝飾器都是一個函數(shù),其實也可以基于類定義裝飾器,看下面的例子:
class Bold(object):
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
return '<b>' + self.func(*args, **kwargs) + '</b>'
@Bold
def hello(name):
return 'hello %s' % name
>>> hello('world')
'<b>hello world</b>'
可以看到,類 Bold 有兩個方法:
__init__():它接收一個函數(shù)作為參數(shù),也就是被裝飾的函數(shù)__call__():讓類對象可調用,就像函數(shù)調用一樣,在調用被裝飾函數(shù)時被調用還可以讓類裝飾器帶參數(shù):
class Tag(object):
def __init__(self, tag):
self.tag = tag
def __call__(self, func):
def wrapped(*args, **kwargs):
return "<{tag}>{res}</{tag}>".format(
res=func(*args, **kwargs), tag=self.tag
)
return wrapped
@Tag('b')
def hello(name):
return 'hello %s' % name
需要注意的是,如果類裝飾器有參數(shù),則 __init__ 接收參數(shù),而 __call__ 接收 func。
前面提到,使用裝飾器有一個瑕疵,就是被裝飾的函數(shù),它的函數(shù)名稱已經不是原來的名稱了,回到最開始的例子:
def makeitalic(func):
def wrapped():
return "<i>" + func() + "</i>"
return wrapped
@makeitalic
def hello():
return 'hello world'
函數(shù) hello 被 makeitalic 裝飾后,它的函數(shù)名稱已經改變了:
>>> hello.__name__
'wrapped'
為了消除這樣的副作用,Python 中的 functools 包提供了一個 wraps 的裝飾器:
from functools import wraps
def makeitalic(func):
@wraps(func) # 加上 wraps 裝飾器
def wrapped():
return "<i>" + func() + "</i>"
return wrapped
@makeitalic
def hello():
return 'hello world'
>>> hello.__name__
'hello'