前言
行為:裝飾器(decorator)可以對(duì)一個(gè)函數(shù)、方法或者類(lèi)進(jìn)行“加工”,相當(dāng)于在封裝。
目的:抽象化代碼,利用函數(shù)是一等公民的特性來(lái)復(fù)用代碼,人生苦短,趕緊偷懶。
須知:越高抽象速度越慢,畢竟函數(shù)的跳轉(zhuǎn)也需要時(shí)間。
本質(zhì)和語(yǔ)法糖
其本質(zhì)是利用函數(shù)作為 python 中的一等公民的特性,可以作為變量來(lái)使用,這時(shí)作為變量時(shí)其實(shí)傳遞的是其函數(shù)的引用(地址)。
也就是說(shuō),只要是將函數(shù)當(dāng)成一等公民的編程語(yǔ)言,其實(shí)都可以寫(xiě)裝飾器。
裝飾器:
def deco(func):
def __funny():
print("2")
func() # 核心
print("3333")
return None # 注意返回 None
return __funny
def foo():
print("foo")
f = deco(foo) # 核心
n = foo()
print(n)

很直白的,首先將 foo 的引用傳遞給 deco 函數(shù),然后 deco 函數(shù)將會(huì)返回一個(gè) __funny() 的引用,所以最后 f 其實(shí)獲取了 “__funny() 的引用”,也就是說(shuō) f() 就是 __funny(),在之后 n = f() 的調(diào)用將會(huì)得到 f() 的返回值,這里我返回了一個(gè) None 值。
利用語(yǔ)法糖 @:
def deco(func):
def __funny():
print("2")
func() # 核心
print("3333")
return None # 注意返回 None
return __funny
@deco
def foo():
print("foo")
n = f()
print(n)

語(yǔ)法糖其實(shí)就是進(jìn)行了 f = deco(foo) 的操作,一句話就是懶。
注意裝飾器內(nèi)部的 self 用于指明這是哪一個(gè)類(lèi)的實(shí)例,所以j即使寫(xiě)在類(lèi)外部也不能省略,因?yàn)閷?shí)際運(yùn)行會(huì)被暴露出來(lái)啦。
與默認(rèn)參數(shù)和關(guān)鍵字參數(shù)的友好會(huì)晤
與默認(rèn)參數(shù)和關(guān)鍵參數(shù)的友好結(jié)合,將會(huì)大大的提高靈活性,能夠變得更加的懶惰。
def deco(func):
def inner(self, *argv, **kwargv):
print("2")
r = func(self, *argv, **kwargv)
print("3333")
return None
return inner
class Something():
@deco
def foo(self):
print("foo")
a = Something()
print(a.foo())
image.png

來(lái)實(shí)際抽象一波
我曾經(jīng)封裝過(guò)一些 python 中的 SQL 方法,其中有一些非常無(wú)聊的操作:
import MySQLdb
class MySqlSearch():
def __init__(self):
pass
def get_conn(self):
self.conn = MySQLdb.connect(
# ...
)
def conn_close(self):
if self.conn:
self.conn.close()
def get_one(self, order='id'):
self.get_conn()
# ...
self.conn_close()
return result
def get_all(self, order='id'):
self.get_conn()
# ...
self.conn_close()
return result
def get_by_page(self, page=1, page_size=10, order='id'):
'''根據(jù)頁(yè)面顯示數(shù)據(jù),默認(rèn)第一頁(yè)起算,一頁(yè)有十行數(shù)據(jù)'''
self.get_conn()
# ...
self.conn_close()
return result
def main():
obj = MySqlSearch()
print(obj.get_one())
print('-'*50)
print(obj.get_all())
print('-'*50)
print(obj.get_by_page())
if __name__ == '__main__':
main()
啊,十分明顯的,為了不長(zhǎng)時(shí)間占用與數(shù)據(jù)庫(kù)的鏈接,每次我都需要開(kāi)關(guān)數(shù)據(jù)庫(kù)的鏈接,太麻煩了。
很明顯,可以將開(kāi)關(guān)數(shù)據(jù)庫(kù)操作給封裝掉:
import MySQLdb
def mysql_open_close_decorator(func):
def __foo(self):
self.conn = MySQLdb.connect(
# ...
)
result = func(self) # 實(shí)際操作
if self.conn:
self.conn.close()
return result
return __foo
class MySqlSearch():
def __init__(self):
pass
@mysql_open_close_decorator
def get_one(self, order='id'):
# ...
return result
@mysql_open_close_decorator
def get_all(self, order='id'):
# ...
return result
@mysql_open_close_decorator
def get_by_page(self, page=1, page_size=10, order='id'):
'''根據(jù)頁(yè)面顯示數(shù)據(jù),默認(rèn)第一頁(yè)起算,一頁(yè)有十行數(shù)據(jù)'''
# ...
return result
def main():
obj = MySqlSearch()
print(obj.get_one())
print('-'*50)
print(obj.get_all())
print('-'*50)
print(obj.get_by_page())
if __name__ == '__main__':
main()
保留被裝飾函數(shù)的元信息
問(wèn)題:假設(shè)你寫(xiě)了裝飾器來(lái)裝飾一個(gè)函數(shù),而我們運(yùn)行時(shí)其實(shí)運(yùn)行的是裝飾器并在其中調(diào)用被裝飾的函數(shù),所以被裝飾函數(shù)不是被直接調(diào)用的,這樣一來(lái)重要的元信息比如函數(shù)名稱(chēng)、文檔字符串、注解和參數(shù)簽名等等信息都會(huì)不會(huì)被保留,此時(shí)我們能看到只有直接調(diào)用的裝飾器的元信息。
def deco(func):
def __funny():
'''裝飾器的文檔字符串'''
print("2")
func() # 核心
print("3333")
return None # 注意返回 None
return __funny
@deco
def foo():
'''被裝飾函數(shù)的文檔字符串'''
print("foo")

裝飾一個(gè)函數(shù),核心在于這個(gè)函數(shù)而不是裝飾器,所以我們更希望我們裝飾過(guò)的函數(shù)能夠保留所有的原始信息,可以自己寫(xiě),但更推薦使用 functools 庫(kù)提供的 @wraps 裝飾器。
from functools import wraps
def deco(func):
@wraps(func) # @wraps 裝飾器,注意傳入被裝飾函數(shù)來(lái)保留其元信息
def __funny():
'''裝飾器的文檔字符串 '''
print("2")
func() # 核心
print("3333")
return None # 注意返回 None
return __funny
@deco
def foo():
'''被裝飾函數(shù)的文檔字符串'''
print("foo")

@wraps 有一個(gè)重要特點(diǎn)是它能讓你通過(guò)屬性 wrapped 來(lái)直接訪問(wèn)被包裝函數(shù),比如上圖中的 foo.wrapped()。
functools 所提供的 wraps 作用于裝飾器,保留被裝飾函數(shù)的元信息和提供一份裝飾器的代碼副本。