文章目錄
裝飾器函數其實是這樣一個接口約束,它必須接受一個 callable 對象作爲參數,然後返回一個 callable 對象,其作用就是爲已經存在的函數或對象添加額外的功能。
函數裝飾器
基本函數裝飾器
Talk is cheap, show me the code.所以,下面先給出一個最簡單的例子,再來解釋裝飾器原理。
def log_it(func):
def wrapper(*args, **kwargs):
print("[Debug]: enter function {}()".format(func.__name__))
return func(*args, **kwargs) # if function func has return, remember to return
return wrapper
@log_it
def add_number(*args):
return sum(args)
print(add_number(1, 2, 3, 4))
print(add_number.__name__)
# 輸出
[Debug]: enter function add_number()
10
wrapper
細心的你會發現,print(add_number.__name__)
輸出的是wrapper,意味着裝飾器的實際意思就是 add_number = log_it(add_number)
,所以執行add_number()
函數就相當於執行wrapper()
函數,而這也就是裝飾器的原理啦。即通過傳入一個函數將其包裝成一個新的函數,賦予它額外的功能。
傳參函數裝飾器
裝飾器還有更大的靈活性,例如帶參數的裝飾器,比如上面的例子中,你想控制print
語句的內容是動態的,那麼你可以給它傳入參數,代碼如下:
def log_it2(level='Debug'):
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
@log_it2(level='Info')
def add_number2(*args):
return sum(args)
print(add_number2(1, 2, 3, 4, 5))
# 輸出
[Info]: enter function add_number2()
15
看到這裏我估計有朋友要暈了,因爲這個裝飾器居然嵌套了三個函數定義,什麼鬼?是不是瞬間感覺還不如學習C++,根本不存在函數嵌套,哈哈~~接下來來仔細分析一下:
- 首先我們要清楚相比第一個例子,我們多加了一層函數嵌套,且這一層函數嵌套就是也僅僅是爲了給裝飾器傳遞參數,那麼不難想到
add_number2.__name__
應該是inner_wrapper(不信你可以輸出看一看); - 基於第1點,我們應該可以推理出它的實際調用是
add_number2 = log_it2(debug='info')(add_number2)
, 從而說明add_number2.__name__
的值就是inner_wrapper.
類裝飾器
類裝飾器和函數裝飾器一樣,包括基本類裝飾器和傳參類裝飾器兩種類型,但是其形式略有不同,類裝飾器必須實現魔法方法__call__
函數,關於__call__
函數以及更多魔法方法的用法請參考Python面向對象、魔法方法
基本類裝飾器
示例代碼如下:
class LogIt(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)
@LogIt
def add_number(*args):
return sum(args)
print(add_number(1, 2, 3, 4))
我上面說形式上略有不同,可這一看,好像卻是大有不同,這又是怎麼實現的呢?首先LogIt類實現了構造方法__init__(self,func)
,它接收一個函數,這看起來很正常;但是,它還是實現了__call__
方法並返回了函數func
,這不正是我們開篇說的,接受一個函數並返回一個函數嗎?這就是它的實現原理。我們可以驗證一下:
>>>add_number
<__main__.LogIt object at 0x10e4dd7f0>
看上面的輸出,add_number
變成一個函數了,所以我們不難想象,它實際過程是 add_number = LogIt(add_number),如此一來其自然而然就是LogIt的一個實例了。
傳參類裝飾器
那麼類裝飾器要怎麼傳遞參數呢?請看下面的示例代碼:
class LogIt2(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__))
return func(*args, **kwargs)
return wrapper
@LogIt2(level='INFO')
def add_number2(*args):
return sum(args)
print(add_number2(1, 2, 3, 4, 5))
如果你融會貫通了上面所蘊含的思想,應該不難推理出它的包裝過程就是:add_number2 = LogIt('INFO')(add_number2)
,所以add_number2
此時應該是實際上是wrapper
函數。怎麼知道我說的是對的呢?
>>> add_number2.__name__
wrapper
以上就是裝飾器的一些基本使用了,多看幾遍,多實踐幾次,你一定可以看懂!
裝飾器執行順序
以上我們只討論了一個裝飾器的使用,如果同時使用多個裝飾器,會是怎樣的執行流程呢?我們先直接上一個例子,看一看:
def decorator_a(func):
print('Get in decorator_a')
def inner_a(*args, **kwargs):
print('Get in inner_a')
return func(*args, **kwargs)
return inner_a
def decorator_b(func):
print('Get in decorator_b')
def inner_b(*args, **kwargs):
print('Get in inner_b')
return func(*args, **kwargs)
return inner_b
@decorator_b
@decorator_a
def f(x):
print('Get in f')
return x * 2
res = f(1)
print('res:', res)
# 輸出結果
# Get in decorator_a
# Get in decorator_b
# Get in inner_b
# Get in inner_a
# Get in f
# res: 2
如果你是第一次看裝飾器,且你的預想中也是這個輸出,那麼恭喜你,你一定是996ICU友情鏈接的天選之人;如果你看的目瞪口呆,其實也不要緊,只要你有995ICU友情鏈接2的精神,你也可以是王者。好吧,回到正題,其實關於多裝飾器執行順序的規則就是從裏到外順序執行,即最先調用最裏層的裝飾器,最後調用最外層的裝飾器。其調用過程爲:
f = decorator_b(decorator_a(f))
且最後f
的形式爲:
# 注意下面不是可執行代碼,縮進是爲了表示邏輯關係
def inner_b(*args, **kwargs):
print('Get in inner_b')
# -----------inner_a-------------
print('Get in inner_a')
# -----------f-------------
print('Get in f')
return x * 2
我覺得,如果你看懂了上面這一段"僞代碼",那麼你就真的理解了裝飾器原理。如果沒看懂,不要緊,試着多看幾遍就好;就好比心情不好,那就去吃一頓火鍋,如果不夠,那就再來一頓!
內置裝飾器
在python中有以下幾個常見的內置裝飾器,它們都和python面向對象編程有關,下面分別做簡要介紹:
@abstractmethod
這是python中抽象類"虛方法"的定義方式,一個類中如果存在@abc.abstractmethod裝飾的方法,那麼其不可以實例化對象,且繼承的子類必須實現@abc.abstractmethod裝飾的方法。
import abc
class A(object):
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def load(self, _input):
pass
@abc.abstractmethod
def save(self, output, data):
pass
class B(A):
def load(self, _input):
return _input.read()
def save(self, output, data):
return output.write(data)
if __name__ == '__main__':
print(issubclass(B, A))
print(isinstance(B(), A))
print(A.__subclasses__())
# 輸出
True
True
[<class '__main__.B'>]
@property
類屬性有三個裝飾器: setter
, getter
, deleter
,它們都是在 property ()
的基礎上做了一些封裝,其中getter
裝飾器和不帶 getter
的屬性裝飾器效果是一樣的。該特性最重要的功能之一就是能實現屬性的參數檢驗。
class Student(object):
@property
def score(self):
return self._score
@score.setter
def score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must between 0 ~ 100!')
self._score = value
@score.deleter
def score(self):
del self._score
s = Student()
s.score = 10
print(s.score)
@classmethod
被@classmethod
裝飾的函數不需要實例化,不需要 self 參數,但第一個參數需要是表示自身類的 cls 參數,可以來調用類的屬性,類的方法,實例化對象等。
class A(object):
# 屬性默認爲類屬性(可以給直接被類本身調用)
num = "類屬性"
# 實例化方法(必須實例化類之後才能被調用)
def func1(self): # self : 表示實例化類後的地址id
print("func1")
print(self)
# 類方法(不需要實例化類就可以被類本身調用)
@classmethod
def func2(cls): # cls : 表示沒用被實例化的類本身
print("func2")
print(cls)
print(cls.num)
cls().func1()
# 不傳遞傳遞默認self參數的方法(該方法也是可以直接被類調用的,但是這樣做不標準)
def func3():
print("func3")
print(A.num) # 屬性是可以直接用類本身調用的
# A.func1() 這樣調用是會報錯:因爲func1()調用時需要默認傳遞實例化類後的地址id參數,如果不實例化類是無法調用的
A.func2()
A.func3()
@staticmethod
被@staticmethod
修飾的方法是靜態方法,靜態方法的參數可以根據業務需求傳入,沒有固定參數;然而前面的實例化方法第一個參數必須是self
,類方法第一個參數必須是cls
。靜態方法,跟普通函數沒什麼區別,與類和實例都沒有所謂的綁定關係,它只不過是碰巧存在類中的一個函數而已,不論是通過類還是實例都可以引用該方法。之所以將其放在類中,是因爲該方法僅爲這個類服務。
class Method(object):
def __init__(self, data):
pass
@staticmethod
def static_method():
print "This is static method in class Method"
內置裝飾器小結
爲了更好的理解辨明實例方法、類方法、靜態方法、抽象方法,我再舉一個例子,從本質上來理一理它們之間的關係:
import abc
class ICU996():
@staticmethod
def static_m(self):
pass
@classmethod
def class_m(cls):
pass
def instance_m(self):
pass
@abc.abstractmethod
def abstract_m(self):
pass
>>>icu = ICU996()
>>>icu.static_m
<function ICU996.static_m at 0x11af930d0>
>>>icu.class_m
<bound method ICU996.class_m of <class 'test.ICU996'>>
>>>icu.instance_m
<bound method ICU996.instance_m of <test.ICU996 object at 0x11af54978>>
>>>icu.abstract_m
<bound method ICU996.abstract_m of <test.ICU996 object at 0x11af54978>>
從上面的輸出結果不難看出,實例化方法和抽象方法本質上是一樣的,都是綁定在實例上的方法;而類方法這是綁定在類上的方法;靜態方法則實質上就是一個不同函數。
裝飾器屬性還原
最後,我們來解決前面的出現的一個問題,在第一個實例中,我們發現被裝飾的函數add_number.__name__
變成了wrapper,那麼如果我們並不想讓這樣的事情發生,我們該怎麼做呢?其實,這一點,可以用內置的裝飾器wraps
來實現:
from functools import wraps
def log_it(func):
@wraps(func) # comment this line to see the diff
def wrapper(*args, **kwargs):
print("[Debug]: enter function {}()".format(func.__name__))
return func(*args, **kwargs)
return wrapper
@log_it
def add_number(*args):
"""
add numbers in tuple
:param args: should be numbers
:return: the sum of tuple
"""
return sum(args)
print(add_number.__name__, add_number.__doc__)
# 輸出
add_number
add numbers in tuple
:param args: should be numbers
:return: the sum of tuple
它不僅可以使函數名保持不變,還可以保持函數原有的doc string。好,那麼是不是應該思考一下它是怎樣實現的呢?
from functools import partial
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
'__annotations__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper,
wrapped,
assigned=WRAPPER_ASSIGNMENTS,
updated=WRAPPER_UPDATES):
for attr in assigned:
try:
value = getattr(wrapped, attr)
except AttributeError:
pass
else:
setattr(wrapper, attr, value)
for attr in updated:
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
# Issue #17482: set __wrapped__ last so we don't inadvertently copy it
# from the wrapped function when updating __dict__
wrapper.__wrapped__ = wrapped
# Return the wrapper so this can be used as a decorator via partial()
return wrapper
def wraps(wrapped,
assigned=WRAPPER_ASSIGNMENTS,
updated=WRAPPER_UPDATES):
return partial(update_wrapper, wrapped=wrapped,
assigned=assigned, updated=updated)
這是python官方的實現方式,關於__module__
,__name__
等特殊屬性,請參考python特殊屬性、魔法方法;這裏包裝的過程簡化來看就是add_number = update_wrapper(wrapper, add_number)
,其中update_wrapper
的功能是更新wrapper函數的一些特殊屬性。
寫在篇後
裝飾器是python的一大難點,它本質上就是一個函數,它可以讓其他函數在不需要變動的情況下增加額外的功能。裝飾器常用於有切面的應用場景,如插入日誌、性能測試等場景。多看、多試、多用,裝飾器,其實也不難。