我們知道,在 Python 中,我們可以像使用變數一樣使用函式:
- 函式可以被賦值給其他變數
- 函式可以被刪除
- 可以在函式裡面再定義函式
- 函式可以作為引數傳遞給另外一個函式
- 函式可以作為另一個函式的返回
簡而言之,函式就是一個物件。
對一個簡單的函式進行裝飾
為了更好地理解裝飾器,我們先從一個簡單的例子開始,假設有下面的函式:
1 2 |
def hello(): return 'hello world' |
現在我們想增強 hello()
函式的功能,希望給返回加上 HTML 標籤,比如 hello world
,但是有一個要求,不改變原來 hello()
函式的定義。這裡當然有很多種方法,下面給出一種跟本文相關的方法:
1 2 3 4 |
def makeitalic(func): def wrapped(): return "<i>" + func() + "</i>" return wrapped |
在上面的程式碼中,我們定義了一個函式 makeitalic
,該函式有一個引數 func
,它是一個函式;在 makeitalic
函式裡面我們又定義了一個內部函式 wrapped
,並將該函式作為返回。
現在,我們就可以不改變 hello()
函式的定義,給返回加上 HTML 標籤了:
1 2 3 |
>>> hello = makeitalic(hello) # 將 hello 函式傳給 makeitalic >>> hello() '<i>hello world</i>' |
在上面,我們將 hello
函式傳給 makeitalic
,再將返回賦給 hello
,此時呼叫 hello()
就得到了我們想要的結果。
不過要注意的是,由於我們將 makeitalic
的返回賦給了 hello
,此時 hello()
函式仍然存在,但是它的函式名不再是 hello 了,而是 wrapped,正是 makeitalic
返回函式的名稱,可以驗證一下:
1 2 |
>>> hello.__name__ 'wrapped' |
對於這個小瑕疵,後文將會給出解決方法。
現在,我們梳理一下上面的例子,為了增強原函式 hello
的功能,我們定義了一個函式,它接收原函式作為引數,並返回一個新的函式,完整的程式碼如下:
1 2 3 4 5 6 7 8 9 |
def makeitalic(func): def wrapped(): return "<i>" + func() + "</i>" return wrapped def hello(): return 'hello world' hello = makeitalic(hello) |
事實上,makeitalic
就是一個裝飾器(decorator),它『裝飾』了函式 hello
,並返回一個函式,將其賦給 hello
。
一般情況下,我們使用裝飾器提供的 @
語法糖(Syntactic Sugar),來簡化上面的寫法:
1 2 3 4 5 6 7 8 |
def makeitalic(func): def wrapped(): return "<i>" + func() + "</i>" return wrapped @makeitalic def hello(): return 'hello world' |
像上面的情況,可以動態修改函式(或類)功能的函式就是裝飾器。本質上,它是一個高階函式,以被裝飾的函式(比如上面的 hello)為引數,並返回一個包裝後的函式(比如上面的 wrapped)給被裝飾函式(hello)。
裝飾器的使用形式
- 裝飾器的一般使用形式如下:
1 2 3 |
@decorator def func(): pass |
等價於下面的形式:
1 2 3 |
def func(): pass func = decorator(func) |
- 裝飾器可以定義多個,離函式定義最近的裝飾器先被呼叫,比如:
1 2 3 4 |
@decorator_one @decorator_two def func(): pass |
等價於:
1 2 3 4 |
def func(): pass func = decorator_one(decorator_two(func)) |
- 裝飾器還可以帶引數,比如:
1 2 3 |
@decorator(arg1, arg2) def func(): pass |
等價於:
1 2 3 4 |
def func(): pass func = decorator(arg1, arg2)(func) |
下面我們再看一些具體的例子,以加深對它的理解。
對帶引數的函式進行裝飾
前面的例子中,被裝飾的函式 hello()
是沒有帶引數的,我們看看被裝飾函式帶引數的情況。對前面例子中的 hello()
函式進行改寫,使其帶引數,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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) |
由於函式 hello
帶引數,因此內嵌包裝函式 wrapped
也做了一點改變:
- 內嵌包裝函式的引數傳給了
func
,即被裝飾函式,也就是說內嵌包裝函式的引數跟被裝飾函式的引數對應,這裡使用了(*args, **kwargs)
,是為了適應可變引數。
看看使用:
1 2 3 4 |
>>> hello('python') '<i>hello python</i>' >>> hello2('python', 'java') '<i>hello python, java</i>' |
帶引數的裝飾器
上面的例子,我們增強了函式 hello 的功能,給它的返回加上了標籤 <i>…</i>,現在,我們想改用標籤 <b>…</b> 或 <p>…</p>。是不是要像前面一樣,再定義一個類似 makeitalic 的裝飾器呢?其實,我們可以定義一個函式,將標籤作為引數,返回一個裝飾器,比如:
1 2 3 4 5 6 7 8 |
def wrap_in_tag(tag): def decorator(func): def wrapped(*args, **kwargs): ret = func(*args, **kwargs) return '<' + tag + '>' + ret + '</' + tag + '>' return wrapped return decorator |
現在,我們可以根據需要生成想要的裝飾器了:
1 2 3 4 5 6 7 8 |
makebold = wrap_in_tag('b') # 根據 'b' 返回 makebold 生成器 @makebold def hello(name): return 'hello %s' % name >>> hello('world') '<b>hello world</b>' |
上面的形式也可以寫得更加簡潔:
1 2 3 |
@wrap_in_tag('b') def hello(name): return 'hello %s' % name |
這就是帶引數的裝飾器,其實就是在裝飾器外面多了一層包裝,根據不同的引數返回不同的裝飾器。
多個裝飾器
現在,讓我們來看看多個裝飾器的例子,為了簡單起見,下面的例子就不使用帶引數的裝飾器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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
進行裝飾,上面的最後幾行程式碼相當於:
1 2 3 4 |
def hello(): return 'hello world' hello = makebold(makeitalic(hello)) |
呼叫函式 hello
:
1 2 |
>>> hello() '<b><i>hello world</i></b>' |
基於類的裝飾器
前面的裝飾器都是一個函式,其實也可以基於類定義裝飾器,看下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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__()
:它接收一個函式作為引數,也就是被裝飾的函式__call__()
:讓類物件可呼叫,就像函式呼叫一樣,在呼叫被裝飾函式時被呼叫
還可以讓類裝飾器帶引數:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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 |
需要注意的是,如果類裝飾器有引數,則 __init__
接收引數,而 __call__
接收 func
。
裝飾器的副作用
前面提到,使用裝飾器有一個瑕疵,就是被裝飾的函式,它的函式名稱已經不是原來的名稱了,回到最開始的例子:
1 2 3 4 5 6 7 8 |
def makeitalic(func): def wrapped(): return "<i>" + func() + "</i>" return wrapped @makeitalic def hello(): return 'hello world' |
函式 hello
被 makeitalic
裝飾後,它的函式名稱已經改變了:
1 2 |
>>> hello.__name__ 'wrapped' |
為了消除這樣的副作用,Python 中的 functool 包提供了一個 wraps 的裝飾器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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' |
1 |
<span style="font-family: 'Microsoft YaHei', 宋體, 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size: 28px;font-style: normal;font-weight: normal;background-color: #ffffff">小結</span> |
- 本質上,裝飾器就是一個返回函式的高階函式。
- 裝飾器可以動態地修改一個類或函式的功能,通過在原有的類或者函式上包裹一層修飾類或修飾函式實現。
- 事實上,裝飾器就是閉包的一種應用,但它比較特別,接收被裝飾函式為引數,並返回一個函式,賦給被裝飾函式,閉包則沒這種限制。