Python 裝飾器(decorator)是一種語法糖(Syntactic sugar),
@d
def foo():
pass
以上代碼和以下代碼是等價的:
foo = d(foo)
進一步說,
@d(param)
def foo():
pass
和下面代碼是等價的
foo = (d(param))(foo)
他們的實質都是傳入一個函數(shù)對象,進行“裝飾”后返回裝飾好的另一個函數(shù)對象。
返回函數(shù)對象的函數(shù)
Python 中的函數(shù)是一個對象(object),它可以像任何其他對象一樣被創(chuàng)建、引用、傳遞和銷毀。引用函數(shù)對象的方法是使用它的函數(shù)名,如 foo 引用的是一個函數(shù)對象,而 foo() 則是其返回值了。
得益于函數(shù)在 Python 中一等公民的地位,你可以在一個函數(shù)中返回另一個函數(shù),接受函數(shù)作為參數(shù)或返回函數(shù)的函數(shù),稱為高階函數(shù)(higher-order function)。
看下面的例子:
def f(a):
def g(b):
def h(c):
print(a, b, c)
return h
return g
In [2]: f(1)(2)(3)
1 2 3
In [3]: g = f(4)
In [4]: g(5)(6)
4 5 6
這是在函數(shù)式編程中被稱為柯里化(currying)的東西,被柯里化后的高階函數(shù) f 其實是這樣執(zhí)行的:
((f(1))(2))(3)
首先執(zhí)行的 f(1),返回其內(nèi)部定義的函數(shù)對象 g,接著執(zhí)行 g(2),返回了 g 內(nèi)部定義的對象 h,以此類推。
不帶參數(shù)的裝飾器函數(shù)
最簡單的情形就是不帶參數(shù)的裝飾器函數(shù)。
給函數(shù) foo 使用裝飾器 d,等價于
foo = d(foo)
為了定義裝飾器 d,必須使其接受一個函數(shù)對象參數(shù),并返回一個函數(shù)對象,而要返回一個函數(shù)對象,就需要在 d 內(nèi)部新定義一個函數(shù)對象。這個內(nèi)部的函數(shù)對象有什么特點呢?它接受 foo 的參數(shù),運行 foo 的代碼,并返回 foo 的返回值,當然還做了裝飾器需要做的額外工作。
按照以上需求,裝飾器 d 的代碼就可以寫出了。
def d(f):
def wrapper(*args, **kwargs):
# do something
res = f(*args, **kwargs)
# do something
return res
return wrapper
d(foo) 返回了函數(shù)對象 wrapper,而變量 foo 指向了它。foo 不再是原來的那個 foo,裝飾器賦予了它額外的功能。
如果需要的話,d 其實是可以這樣被調(diào)用的:
In [35]: def foo(n):
...: return n + 1
...:
In [36]: d(foo)(2)
Out[36]: 3
是不是就是上面柯里化的形式?
另一種裝飾器就更容易理解了:
def d(f):
f.__some_attr__ = 'attr'
return f
根本就沒有重新定義一個函數(shù)對象,改了 f 的某個屬性就把它扔出來了。
帶參數(shù)的裝飾器函數(shù)
帶參數(shù)的裝飾器函數(shù)這樣使用:
@d(param)
def foo(n):
return n + 1
它等價于
foo = d(param)(foo)
為此,首先需要定義一個接受裝飾器參數(shù) param 的函數(shù),接著再定義接受 foo 為參數(shù)的函數(shù),最后是接受函數(shù) foo 本身參數(shù)為參數(shù)的函數(shù),除了最內(nèi)層函數(shù)返回 foo() 運行結果外,其他函數(shù)都返回一個函數(shù)對象。
def d(param):
def outter(f):
def inner(*args, **kwargs):
# do something
res = f(*args, **kwargs)
# do something
return res
return inner
return outter
你當然也可以這樣調(diào)用 d:
d(param)(foo)(3)
callable
如果一個類的實例 instance 具有 __call__ 方法,那么這個實例就是一個 callable,instance() 就相當于 instance.__call__()。
不帶參數(shù)的裝飾器類
仍然沿用以上對裝飾器語法糖的解釋,對函數(shù) foo 使用裝飾器類 D,等價于
foo = D(foo)
foo 對象應該在 D.__init__() 中被傳入,得到一個 callable,而 foo 在 D.__call__() 中被執(zhí)行。所以一個不帶參數(shù)的裝飾器類應該寫成這樣:
class D(object):
def __init__(self, f):
self.f = f
def __call__(self, *args, **kwargs):
# do something
res = self.f(*args, **kwargs)
# do something
return res
帶參數(shù)的裝飾器類
使用帶參數(shù)的裝飾器類,等價于
foo = D(params)(foo)
可以知道,裝飾器的參數(shù) params 在 D.__init__() 中被傳入,而 D.__call__() 接受函數(shù)對象 foo, 返回裝飾后的函數(shù)對象。因而裝飾器類是這樣的:
class D(object):
def __init__(self, param):
self.param = param
def __call__(self, f):
def wrapper(*args, **kwargs):
# do something
res = f(*args, **kwargs)
# do something
return res
return wrapper
總結
本文提供了將裝飾器轉化成高階函數(shù)表達的一種范式,根據(jù)這一范式,可以直接推導出四種裝飾器(帶參/不帶參、函數(shù)/類)的寫法。