上次知識回顧:https://www.cnblogs.com/dotnetcrazy/p/9278573.html 代碼褲子:https://github.com/lotapp/BaseCode 線上編程:https://mybinder.org/v2/gh/lotapp/BaseCode/mast ...
上次知識回顧:https://www.cnblogs.com/dotnetcrazy/p/9278573.html
代碼褲子:https://github.com/lotapp/BaseCode
線上編程:https://mybinder.org/v2/gh/lotapp/BaseCode/master
線上預覽:http://github.lesschina.com/python/base/ext/基礎拓展.html
終於期末考試結束了,聰明的小明同學現在當然是美滋滋的過暑假了,左手一隻瓜,右手一本書~正在給老鄉小張同學拓展他研究多日的知識點
1.NetCore裝飾器模式¶
裝飾器這次從C#
開始引入,上次剛講迭代器模式
,這次把裝飾器模式
也帶一波(純Python方向的可以選擇性跳過,也可以當擴展)
其實通俗講就是,給原有對象動態的添加一些額外的職責(畢竟動不動就改類你讓其他調用的人咋辦?也不符合開放封閉原則是吧~)
舉個簡單的例子:(https://github.com/lotapp/BaseCode/tree/master/netcore/3_Ext/Decorators)
BaseComponent.cs
/// <summary> /// 組件的抽象父類 /// </summary> public abstract class BaseComponent { /// <summary> /// 定義一個登錄的抽象方法 /// 其他方法,這邊省略 /// </summary> public abstract string Login(); }
LoginComponent.cs
/// <summary> /// 預設登錄組件(賬號+密碼) /// 其他方法省略 /// 友情提醒一下,抽象類裡面可以定義非抽象方法 /// </summary> public class LoginComponent : BaseComponent { public override string Login() { return "預設賬號密碼登錄"; } }
預設調用:
static void Main(string[] args) { var obj = new LoginComponent(); var str = obj.Login(); Console.WriteLine(str); }
如果這時候平臺需要添加微信第三方登錄,怎麼辦?一般都是用繼承來解決,其實還可以通過靈活的裝飾器
來解決:(好處可以自己體會)
先定義一個通用裝飾器(不一定針對登錄,註冊等等只要在BaseComponent中的都能用)
/// <summary> /// 裝飾器 /// </summary> public class BaseDecorator : BaseComponent { protected BaseComponent _component; /// <summary> /// 構造函數 /// </summary> /// <param name="obj">登錄組件對象</param> protected BaseDecorator(BaseComponent obj) { this._component = obj; } public override string Login() { string str = string.Empty; if (_component != null) str = _component.Login(); return str; } }
現在根據需求添加微信登錄:(符合開放封閉原則)
/// <summary> /// 預設登錄組件(賬號+密碼) /// 其他方法省略 /// </summary> public class WeChatLoginDecorator : BaseDecorator { public WeChatLoginDecorator(BaseComponent obj) : base(obj) { } /// <summary> /// 添加微信第三方登錄 /// </summary> /// <returns></returns> public string WeChatLogin() { return "add WeChatLogin"; } }
調用:(原有系統該怎麼用就怎麼用,新系統可以使用裝飾器來添加新功能)
static void Main(string[] args) { #region 登錄模塊V2 // 實例化登錄裝飾器 var loginDecorator = new WeChatLoginDecorator(new LoginComponent()); // 原有的登錄方法 var str1 = loginDecorator.Login(); // 現在新增的登錄方法 var str2 = loginDecorator.WeChatLogin(); Console.WriteLine($"{str1}\n{str2}"); #endregion }
結果:
預設賬號密碼登錄
add WeChatLogin
如果再加入QQ和新浪登錄的功能就再添加一個V3版本的裝飾器,繼承當時V2版本的登錄即可(版本迭代特別方便)
/// <summary> /// 預設登錄組件(賬號+密碼) /// 其他方法省略 /// </summary> public class LoginDecoratorV3 : WeChatLoginDecorator { public LoginDecoratorV3(BaseComponent obj) : base(obj) { } /// <summary> /// 添加QQ登錄 /// </summary> /// <returns></returns> public string QQLogin() { return "add QQLogin"; } /// <summary> /// 添加新浪登錄 /// </summary> /// <returns></returns> public string SinaLogin() { return "add SinaLogin"; } }
調用:
static void Main(string[] args) { #region 登錄模塊V3 // 實例化登錄裝飾器 var loginDecoratorV3 = new LoginDecoratorV3(new LoginComponent()); // 原有的登錄方法 var v1 = loginDecoratorV3.Login(); // 第二個版本迭代中的微信登錄 var v2 = loginDecoratorV3.WeChatLogin(); // 新增的QQ和新浪登錄 var qqLogin = loginDecoratorV3.QQLogin(); var sinaLogin = loginDecoratorV3.SinaLogin(); Console.WriteLine($"{v1}\n{v2}\n{qqLogin}\n{sinaLogin}"); #endregion }
結果:
預設賬號密碼登錄
add WeChatLogin
add QQLogin
add SinaLogin
其實還有很多用處,比如原有系統緩存這塊當時考慮不到,現在併發來了,已經上線了,原有代碼又不太敢大幅度修改,這時候裝飾器就很方便的給某些功能添加點緩存、測試、日記等等系列功能(AOP裡面很多這種概念)
實際場景說的已經很明白了,其他的自己摸索一下吧
2.Python裝飾器¶
那Python怎麼實現裝飾器呢?小胖問道。
小明屁顛屁顛的跑過去說道,通過閉包咯~(閉包如果忘了,可以回顧一下)
2.1.裝飾器引入¶
來看一個應用場景,以前老版本系統因為併發比較小,沒考慮到緩存
def get_data(): print("直接資料庫讀取數據") def main(): get_data() if __name__ == '__main__': main()
在不修改原有代碼的前提下咋辦?我們參照C#和Java寫下如下代碼:
In [1]:# 添加一個閉包 def cache(func): def decorator(): print("給功能添加了緩存") if True: pass else: func()# 如果緩存失效則讀取資料庫獲取新的數據 return decorator def get_data(): print("直接資料庫讀取數據") def main(): f1 = cache(get_data) f1() print(type(f1)) if __name__ == '__main__': main()
給功能添加了緩存 <class 'function'>
小張問道:“怎麼也這麼麻煩啊,C#的那個我就有點暈了,怎麼Python也這樣啊?”f1 = cache(get_data)
f1()
小明哈哈一笑道:“人生苦短,我用Python~這句話可不是隨便說著玩的,來來來,看看Python的語法糖”:
In [2]:def cache(func): def wrapper(): print("給功能添加了緩存") if True: pass else: func() # 如果緩存失效則讀取資料庫獲取新的數據 return wrapper @cache def get_data(): print("直接資料庫讀取數據") def main(): get_data() if __name__ == '__main__': main()
給功能添加了緩存
其實
@cache def get_data()
等價於
# 把f1改成函數名字罷了。可以這麼理解:get_data重寫指向了一個新函數 get_data = cache(get_data)
小張同學瞪了瞪眼睛,努力回想著以前的知識點,然後脫口而出:“這不是我們之前講的屬性裝飾器嗎?而且好方便啊,這完全符合開放封閉原則啊!“
class Student(object): def __init__(self, name, age): # 一般需要用到的屬性都直接放在__init__裡面了 self.name = name self.age = age @property def name(self): return self.__name @name.setter def name(self, name): self.__name = name @property def age(self): return self.__age @age.setter def age(self, age): if age > 0: self.__age = age else: print("age must > 0") def show(self): print("name:%s,age:%s" % (self.name, self.age))
小明也愣了愣,說道:”也對哦,你不說我都忘了,我們學習面向對象三大特性的時候經常用呢,怪不得這麼熟悉呢“
隨後又嘀咕了一句:”我怎麼不知道開放封閉原則...“
小張嘲笑道:”這你都不知道?對擴展開放,對已經實現的代碼封閉嘛~“
In [3]:# 需要註意一點 def cache(func): print("裝飾器開始裝飾") def wrapper(): print("給功能添加了緩存") if True: pass else: func() # 如果緩存失效則讀取資料庫獲取新的數據 return wrapper @cache # 當你寫這個的時候,裝飾器就開始裝飾了,閉包裡面的功能是你調用的時候執行 def get_data(): print("直接資料庫讀取數據")
裝飾器開始裝飾
2.2.多個裝飾器¶
小明趕緊扯開話題,”咳咳,我們接下來我們接著講裝飾器"
小張問道,像上面那個第三方登錄的案例,想加多少加多少,Python怎麼辦呢?
小明一笑而過~
現在項目又升級了,要求每次調用都要列印一下日記信息,方便以後糾錯,小張先用自己的理解打下了這段代碼,然後像小明請教:
In [4]:def log(func): def wrapper(): print("輸出日記信息") cache(func)() return wrapper def cache(func): def wrapper(): print("給功能添加了緩存") if True: pass else: func() # 如果緩存失效則讀取資料庫獲取新的數據 return wrapper @log def get_data(): print("直接資料庫讀取數據") def main(): get_data() if __name__ == '__main__': main()
輸出日記信息 給功能添加了緩存
小明剛美滋滋的喝著口口可樂呢,看到代碼後一不小心噴了小張一臉,然後尷尬的說道:“Python又不是只能裝飾一個裝飾器,來看看我的代碼”:
In [5]:def log(func): print("開始裝飾Log模塊") def wrapper(): print("輸出日記信息") func() return wrapper def cache(func): print("開始裝飾Cache模塊") def wrapper(): print("給功能添加了緩存") if True: pass else: func() # 如果緩存失效則讀取資料庫獲取新的數據 return wrapper @log @cache def get_data(): print("直接資料庫讀取數據") def main(): get_data() if __name__ == '__main__': main()
開始裝飾Cache模塊 開始裝飾Log模塊 輸出日記信息 給功能添加了緩存
小張耐心的看完了代碼,然後說道:“咦,我發現它裝飾的時候是從下往上裝飾,執行的時候是從上往下啊?執行的時候程式本來就是從上往下,按照道理應該是從上往下裝飾啊?”
小明神秘的說道:“你猜啊~你可以把它理解為寄快遞和拆快遞”
小張興奮的跳起來了:
裝飾器:裝快遞,先包裝裡面的物品,然後再加個盒子。執行裝飾器:拆快遞,先拆外面的包裝再拆裡面的~簡直妙不可言啊
2.3.帶參裝飾器¶
小明繼續講述他哥哥的血淚歷史:
需求時刻在變,系統使用範圍更廣了,為了不砸場子,摳門的老闆決定每年多花5W在技術研發的硬體支持上,這下子技術部老開心了,想想以前前端只能通過CDN和HTTP請求來緩存,後端只能依賴頁面緩存和資料庫緩存就心塞,於是趕緊新增加一臺Redis的雲伺服器。為了以後和現在緩存代碼得變一變了,需要支持指定的緩存資料庫:(如果不是維護別人搞的老項目,你這麼玩保證被打死,開發的時候老老實實的工廠模式搞起)
帶參數的裝飾器一般都是用來記錄logo日記比較多,自己開發知道debug模式,生產指定except模式等等
In [6]:# 可以理解為,在原來的外面套了一層 def cache(cache_name): def decorator(func): def wrapper(): if cache_name == "redis": print("給功能添加了Redis緩存") elif cache_name == "memcache": pass else: func() return wrapper return decorator @cache("redis") # 相當於是:get_data = cache(”redis“)(get_data) def get_data(): print("直接資料庫讀取數據") def main(): get_data() if __name__ == '__main__': main()
給功能添加了Redis緩存
小張很高興,然後練了練手,然後質問小明道:”你是不是藏了一手!“
代碼如下:
In [7]:def log(func): def inner(): print("%s log_info..." % func.__name__) func() return inner @log def login_in(name_str, pass_str): return "歡迎登錄:%s" % (name_str) @log def login_out(): print("已經退出登錄") @log def get_data(id): print("%s:data xxx" % id) def main(): login_out() get_data(1) print(login_in("小明", "xxx")) if __name__ == '__main__': main()
login_out log_info... 已經退出登錄
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-7-dcb695819107> in <module>() 23 24 if __name__ == '__main__': ---> 25main() <ipython-input-7-dcb695819107> in main() 19 def main(): 20 login_out() ---> 21get_data(1) 22 print(login_in("小明", "xxx")) 23 TypeError: inner() takes 0 positional arguments but 1 was given
2.4.通用裝飾器¶
小明尷尬的笑了下,然後趕緊傾囊相授,定義一個通用的裝飾器:(傳參數就在外面套一層)
def log(func): @functools.wraps(func) # 簽名下麵一個案例就會講 def wrapper(*args,**kv): """可變參 + 關鍵字參數""" print("%s log_info..." % func.__name__) return func(*args,**kv) return wrapper
這部分知識如果忘記了可以回顧一下,我們之前講的函數系列:https://www.cnblogs.com/dotnetcrazy/p/9175950.html
In [8]:def log(func): # 可變參 + 關鍵字參數 def wrapper(*args,**kv): print("%s log_info..." % func.__name__) return func(*args,**kv) return wrapper @log def login_in(name_str, pass_str): return "歡迎登錄:%s" % (name_str) @log def login_out(): print("已經退出登錄") @log def get_data(id): print("%s:data xxx" % id) def main(): login_out() get_data(1) print(login_in("小明", "xxx")) if __name__ == '__main__': main()
login_out log_info... 已經退出登錄 get_data log_info... 1:data xxx login_in log_info... 歡迎登錄:小明
2.5.擴展補充¶
其實裝飾器可以做很多事情,比如強制類型檢測等,先看幾個擴展:
1.裝飾器方法簽名的問題¶
成也裝飾器,敗也裝飾器,來個案例看看,裝飾器裝飾的函數真的就對原函數沒點影響?
In [9]:# 添加一個閉包 def cache(func): def wrapper(*args,**kv): if True: print("緩存尚未失效:直接返回緩存數據") else: func(*args,**kv) return wrapper def get_data(id): """獲取數據""" print("通過%d直接資料庫讀取數據"%id)In [10]:
# 進行裝飾 get_data = cache(get_data) # 調用原有名稱的函數 get_data(110) # 發現雖然函數調用時候的名字沒有變 # 但是內部簽名卻變成了閉包裡面的函數名了 print(get_data.__name__) print(get_data.__doc__) # print(get_data.__annotations__)
緩存尚未失效:直接返回緩存數據 wrapper None
發現雖然函數調用時候的名字沒有變,但是內部簽名卻變成了閉包裡面的函數名了!
玩過逆向的人都知道,像你修改了apk文件,它看似一樣,但簽名就變了,得再處理才可能繞過原來的一些自效驗的驗證措施
這邊一樣的道理,你寫了一個裝飾器作用在某個函數上,但是這個函數的重要的元信息比如名字、文檔字元串、註解和參數簽名都丟失了。
functools
裡面的wraps
就幫我們幹了這個事情(之前講模塊的時候引入了functools,隨後講衍生的時候用了裡面的偏函數,這邊講講wraps
)
上面代碼改改:
In [11]:from functools import wraps # 添加一個閉包 def cache(func): @wraps(func) def wrapper(*args,**kv): if True: print("緩存尚未失效:直接返回緩存數據") else: func(*args,**kv) return wrapper def get_data(id): """獲取數據""" print("通過%d直接資料庫讀取數據"%id) # 進行裝飾 get_data = cache(get_data) # 調用原有名稱的函數 get_data(110) # 簽名已然一致 print(get_data.__name__) print(get_data.__doc__) # print(get_data.__annotations__)
緩存尚未失效:直接返回緩存數據 get_data 獲取數據
另外:@wraps
有一個重要特征是它能讓你通過屬性 __wrapped__
直接訪問被包裝函數,eg:
get_data.__wrapped__(100)
通過100直接資料庫讀取數據
2.裝飾器傳參的擴展(可傳可不傳)¶
In [13]:import logging from functools import wraps, partial def logged(func=None, *, level=logging.DEBUG, name=None, message=None): if func is None: return partial(logged, level=level, name=name, message=message) logname = name if name else func.__module__ log = logging.getLogger(logname) logmsg = message if message else func.__name__ @wraps(func) def wrapper(*args, **kwargs): log.log(level, logmsg) return func(*args, **kwargs) return wrapper @logged def add(x, y): return x + y @logged(level=logging.CRITICAL, name='測試') def get_data(): print("讀數據ing") def main(): add(1,2) get_data() if __name__ == '__main__': main()
get_data
讀數據ing
3.類中定義裝飾器¶
在類裡面定義裝飾器很簡單,但是你首先要確認它的使用方式。比如到底是作為一個實例方法還是類方法:(別忘記寫self
和cls
)
from functools import wraps class A(object): # 實例方法 def decorator1(self, func): @wraps(func) def wrapper(*args, **kwargs): print("實例方法裝飾器") return func(*args, **kwargs) return wrapper # 類方法 @classmethod def decorator2(cls, func): @wraps(func)