前言
行為:裝飾器(decorator)可以對一個函數(shù)焦辅、方法或者類進行“加工”,相當(dāng)于在封裝婚惫。
目的:抽象化代碼氛赐,利用函數(shù)是一等公民的特性來復(fù)用代碼,人生苦短先舷,趕緊偷懶艰管。
須知:越高抽象速度越慢,畢竟函數(shù)的跳轉(zhuǎn)也需要時間蒋川。
本質(zhì)和語法糖
其本質(zhì)是利用函數(shù)作為 python 中的一等公民的特性牲芋,可以作為變量來使用,這時作為變量時其實傳遞的是其函數(shù)的引用(地址)。
也就是說缸浦,只要是將函數(shù)當(dāng)成一等公民的編程語言夕冲,其實都可以寫裝飾器。
裝飾器:
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ù)將會返回一個 __funny() 的引用,所以最后 f 其實獲取了 “__funny() 的引用”卜高,也就是說 f() 就是 __funny()弥姻,在之后 n = f() 的調(diào)用將會得到 f() 的返回值,這里我返回了一個 None 值掺涛。
利用語法糖 @:
def deco(func):
def __funny():
print("2")
func() # 核心
print("3333")
return None # 注意返回 None
return __funny
@deco
def foo():
print("foo")
n = f()
print(n)
語法糖其實就是進行了 f = deco(foo) 的操作庭敦,一句話就是懶。
注意裝飾器內(nèi)部的 self 用于指明這是哪一個類的實例薪缆,所以j即使寫在類外部也不能省略秧廉,因為實際運行會被暴露出來啦。
與默認(rèn)參數(shù)和關(guān)鍵字參數(shù)的友好會晤
與默認(rèn)參數(shù)和關(guān)鍵參數(shù)的友好結(jié)合拣帽,將會大大的提高靈活性疼电,能夠變得更加的懶惰。
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
來實際抽象一波
我曾經(jīng)封裝過一些 python 中的 SQL 方法诞外,其中有一些非常無聊的操作:
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ù)頁面顯示數(shù)據(jù),默認(rèn)第一頁起算灾票,一頁有十行數(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()
啊峡谊,十分明顯的,為了不長時間占用與數(shù)據(jù)庫的鏈接刊苍,每次我都需要開關(guān)數(shù)據(jù)庫的鏈接既们,太麻煩了。
很明顯正什,可以將開關(guān)數(shù)據(jù)庫操作給封裝掉:
import MySQLdb
def mysql_open_close_decorator(func):
def __foo(self):
self.conn = MySQLdb.connect(
# ...
)
result = func(self) # 實際操作
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ù)頁面顯示數(shù)據(jù)啥纸,默認(rèn)第一頁起算,一頁有十行數(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ù)的元信息
問題:假設(shè)你寫了裝飾器來裝飾一個函數(shù)婴氮,而我們運行時其實運行的是裝飾器并在其中調(diào)用被裝飾的函數(shù)斯棒,所以被裝飾函數(shù)不是被直接調(diào)用的,這樣一來重要的元信息比如函數(shù)名稱主经、文檔字符串荣暮、注解和參數(shù)簽名等等信息都會不會被保留,此時我們能看到只有直接調(diào)用的裝飾器的元信息罩驻。
def deco(func):
def __funny():
'''裝飾器的文檔字符串'''
print("2")
func() # 核心
print("3333")
return None # 注意返回 None
return __funny
@deco
def foo():
'''被裝飾函數(shù)的文檔字符串'''
print("foo")
裝飾一個函數(shù)穗酥,核心在于這個函數(shù)而不是裝飾器,所以我們更希望我們裝飾過的函數(shù)能夠保留所有的原始信息,可以自己寫砾跃,但更推薦使用 functools 庫提供的 @wraps 裝飾器骏啰。
from functools import wraps
def deco(func):
@wraps(func) # @wraps 裝飾器,注意傳入被裝飾函數(shù)來保留其元信息
def __funny():
'''裝飾器的文檔字符串 '''
print("2")
func() # 核心
print("3333")
return None # 注意返回 None
return __funny
@deco
def foo():
'''被裝飾函數(shù)的文檔字符串'''
print("foo")
@wraps 有一個重要特點是它能讓你通過屬性 wrapped 來直接訪問被包裝函數(shù)抽高,比如上圖中的 foo.wrapped()判耕。
functools 所提供的 wraps 作用于裝飾器,保留被裝飾函數(shù)的元信息和提供一份裝飾器的代碼副本厨内。