python 实现 享元模式
本文的目錄地址
本文的代碼地址
由于對(duì)象創(chuàng)建的開(kāi)銷,面向?qū)ο蟮南到y(tǒng)可能會(huì)面臨性能問(wèn)題。性能問(wèn)題通常在資源受限的嵌入式系統(tǒng)中出現(xiàn),比如智能手機(jī)和平板電腦。大型復(fù)雜系統(tǒng)中也可能會(huì)出現(xiàn)同樣的問(wèn)題,因?yàn)橐谄渲袆?chuàng)建大量對(duì)象(用戶),這些對(duì)象需要同時(shí)并存。
這個(gè)問(wèn)題之所以會(huì)發(fā)生,是因?yàn)楫?dāng)我們創(chuàng)建一個(gè)新對(duì)象時(shí),需要分配額外的內(nèi)存。雖然虛擬內(nèi)存理論上為我們提供了無(wú)限制的內(nèi)存空間,但現(xiàn)實(shí)卻并非如此。如果一個(gè)系統(tǒng)耗盡了所有的物理內(nèi)存,就會(huì)開(kāi)始將內(nèi)存頁(yè)替換到二級(jí)存儲(chǔ)設(shè)備,通常是硬盤驅(qū)動(dòng)器(Hard Disk Drive,HDD)。在多數(shù)情況下,由于內(nèi)存和硬盤之間的性能差異,這是不能接受的。固態(tài)硬盤(Solid State Drive,SSD)的性能一般比硬盤更好,但并非人人都使用SSD,SSD并不會(huì)很快全面替代硬盤。
除內(nèi)存使用之外,計(jì)算性能也是一個(gè)考慮點(diǎn)。圖形軟件,包括計(jì)算機(jī)游戲,應(yīng)該能夠極快地渲染3D信息(例如,有成千上萬(wàn)顆樹(shù)的森林或滿是士兵的村莊)。如果一個(gè)3D地帶的每個(gè)對(duì)象都是單獨(dú)創(chuàng)建,未使用數(shù)據(jù)共享,那么性能將是無(wú)法接受的。
作為軟件工程師,我們應(yīng)該編寫更好的軟件來(lái)解決軟件問(wèn)題,而不是要求客戶購(gòu)買更多更好的硬件。享元設(shè)計(jì)模式通過(guò)為相似對(duì)象引入數(shù)據(jù)共享來(lái)最小化內(nèi)存使用,提升性能。一個(gè)享元(Flyweight)就是一個(gè)包含狀態(tài)獨(dú)立的不可變(又稱固有的)數(shù)據(jù)的共享對(duì)象。依賴狀態(tài)的可變(又稱非固有的)數(shù)據(jù)不應(yīng)是享元的一部分,因?yàn)槊總€(gè)對(duì)象的這種信息都不同,無(wú)法共享。如果享元需要非固有的數(shù)據(jù),應(yīng)該由客戶端代碼顯示地提供。
用一個(gè)例子可能有助于解釋實(shí)際場(chǎng)景中如何使用享元模式。假設(shè)我們正在設(shè)計(jì)一個(gè)性能關(guān)鍵的游戲,例如第一人稱射擊(First-Person Shooter,FPS)游戲。在FPS游戲中,玩家(士兵)共享一些狀態(tài),如外在表現(xiàn)和行為。例如,在《反恐精英》游戲中,同一團(tuán)隊(duì)(反恐精英或恐怖分子)的所有士兵看起來(lái)都是一樣的(外在表現(xiàn))。同一個(gè)游戲中,(兩個(gè)團(tuán)隊(duì)的)所有士兵都有一些共同的動(dòng)作,比如,跳起、低頭等(行為)。這意味著我們可以創(chuàng)建一個(gè)享元來(lái)包含所有共同的數(shù)據(jù)。當(dāng)然,士兵也有許多因人而異的可變數(shù)據(jù),這些數(shù)據(jù)不是享元的一部分,比如,槍支、健康狀況和地理位置等。
現(xiàn)實(shí)生活中的例子
享元模式(Flyweight pattern)是一個(gè)用于優(yōu)化的設(shè)計(jì)模式。因此,要找一個(gè)合適的現(xiàn)實(shí)生活的例子不太容易。我們可以把享元看做現(xiàn)實(shí)生活中的緩存區(qū)。例如,許多書店都有專用的書架來(lái)擺放最新和流行的出版物。這就是一個(gè)緩存區(qū),你可以先在這些專用書架上看看有沒(méi)有正在找的書籍,如果沒(méi)找到,則可以讓圖書管理員來(lái)幫你。
軟件的例子
Exaile音樂(lè)播放器使用享元來(lái)復(fù)用通過(guò)相同URL識(shí)別的對(duì)象(在這里是指音樂(lè)歌曲)。創(chuàng)建一個(gè)與已有對(duì)象的URL相同的新對(duì)象是沒(méi)有意義的,所以復(fù)用相同的對(duì)象來(lái)節(jié)約資源。
Peppy是一個(gè)用Python語(yǔ)言實(shí)現(xiàn)的類XEmacs編輯器,它使用享元模式存儲(chǔ)major mode狀態(tài)欄的狀態(tài)。這是因?yàn)槌怯脩粜薷?#xff0c;否則所有狀態(tài)欄共享相同的屬性。
應(yīng)用案例
享元旨在優(yōu)化性能和內(nèi)存使用。所有嵌入式系統(tǒng)(手機(jī)、平板電腦、游戲終端和微控制器等)和性能關(guān)鍵的應(yīng)用(游戲、3D圖形處理和實(shí)時(shí)系統(tǒng)等)都能從其獲益。
若想要享元模式有效,需要滿足GoF的《設(shè)計(jì)模式》一書羅列的以下幾個(gè)條件。
- 應(yīng)用需要使用大量的對(duì)象
- 對(duì)象太多,存儲(chǔ)/渲染它們的代價(jià)太大。一旦移除對(duì)象中的可變狀態(tài)(因?yàn)樵谛枰畷r(shí),應(yīng)該由客戶端代碼顯示地傳遞給享元),多組不同的對(duì)象可被相對(duì)更少的共享對(duì)象所替代。
- 對(duì)象ID對(duì)于應(yīng)用不重要。對(duì)象共享會(huì)造成ID比較的失敗,所以不能依賴對(duì)象ID(那些在客戶端代碼看來(lái)不同的對(duì)象,最終具有相同的ID)。
實(shí)現(xiàn)
由于之前已提到樹(shù)的例子,那么就來(lái)看看如何實(shí)現(xiàn)它。在這個(gè)例子中,我們將構(gòu)造一小片水果樹(shù)的森林,小到能確保在單個(gè)終端頁(yè)面中閱讀整個(gè)輸出。然而,無(wú)論你構(gòu)造的森林有多大,內(nèi)存分配都保持相同。下面這個(gè)Enum類型變量描述三種不同種類的水果樹(shù)。
TreeType=Enum('TreeType','apple_tree cherry_tree peach_tree')在深入代碼之前,我們稍稍解釋一下memoization與享元模式之間的區(qū)別。memoization是一種優(yōu)化技術(shù),使用一個(gè)緩存來(lái)避免重復(fù)計(jì)算那些在更早的執(zhí)行步驟中已經(jīng)計(jì)算好的結(jié)果。memoization并不是只能應(yīng)用于某種特定的編程方式,比如面向?qū)ο缶幊?#xff08;Object-Oriented Programming,OOP)。在Python中,memoization可以應(yīng)用于方法和簡(jiǎn)單的函數(shù)。享元?jiǎng)t是一種特定于面向?qū)ο缶幊虄?yōu)化的設(shè)計(jì)模式,關(guān)注的是共享對(duì)象數(shù)據(jù)。
在Python中,享元可以以多種方式實(shí)現(xiàn)。pool變量是一個(gè)對(duì)象池(換句話說(shuō),是我們的緩存)。注意:pool是一個(gè)類屬性(類的所有實(shí)例共享的一個(gè)變量)。使用特殊方法__new__(這個(gè)方法在__init__之前被調(diào)用),我們把Tree類變換成一個(gè)元類,元類支持自引用。這意味著cls引用的是Tree類。當(dāng)客戶端要?jiǎng)?chuàng)建Tree的一個(gè)實(shí)例時(shí),會(huì)以tree_type參數(shù)傳遞樹(shù)的種類。樹(shù)的種類用于檢查是否創(chuàng)建過(guò)相同種類的樹(shù)。如果是,則返回之前創(chuàng)建的對(duì)象;否則,將這個(gè)新的樹(shù)種添加到池中,并返回相應(yīng)的新對(duì)象,如下所示。
def __new__(cls, tree_type):obj=cls.pool.get(tree_type, None)if not obj:obj=object.__new__(cls)cls.pool[tree_type]=objobj.tree_type=tree_typereturn obj方法render()用于在屏幕上渲染一棵樹(shù)。注意,享元不知道的所有可變(外部的)信息都需要客戶端代碼顯示地傳遞。在當(dāng)前案例中,每棵樹(shù)都用到一個(gè)隨機(jī)的年齡和一個(gè)x,y形式的位置。為了讓render()更加有用,有必要確保沒(méi)有樹(shù)會(huì)被渲染到另一顆之上。你可以考慮把這個(gè)作為練習(xí)。如果你想讓渲染更加有趣,可以使用一個(gè)圖形工具包,比如Tkinter或Pygame。
def render(self,age,x,y):print('render a tree of type {} and age {} at ({},{})'.format(self.tree_type,age,x,y))main()函數(shù)展示了我們可以如何使用享元模式。一棵樹(shù)的年齡是1-30年之間的一個(gè)隨機(jī)數(shù)。坐標(biāo)使用1-100之間的隨機(jī)數(shù)。雖然渲染了18棵樹(shù),但僅分配了3顆樹(shù)的內(nèi)存。輸出的最后一行證明當(dāng)使用享元時(shí),我們不能依賴對(duì)象的ID。函數(shù)id()會(huì)返回對(duì)象的內(nèi)存地址。Python規(guī)范并沒(méi)有要求id()返回對(duì)象的內(nèi)存地址,只是要求id()為每個(gè)對(duì)象返回一個(gè)唯一性ID,不過(guò)CPython(Python的官方實(shí)現(xiàn))正好使用對(duì)象的內(nèi)存地址作為對(duì)象唯一性ID。在我們的例子中,即使兩個(gè)對(duì)象看起來(lái)不相同,但是如果它們屬于同一個(gè)享元家族(在這里,家族由tree_type定義),那么它們實(shí)際上有相同的ID。當(dāng)然,不同ID的比較仍然可用于不同家族的對(duì)象,但這僅在客戶端知道實(shí)現(xiàn)細(xì)節(jié)的情況下才可行(通常并非如此)。
def main():rnd=random.Random()age_min,age_max=1,30min_point,max_point=0,100tree_counter=0for _ in range(10):t1=Tree(TreeType.apple_tree)t1.render(rnd.randint(age_min,age_max),rnd.randint(min_point,max_point),rnd.randint(min_point, max_point))tree_counter+=1for _ in range(3):t2=Tree(TreeType.cherry_tree)t2.render(rnd.randint(age_min,age_max),rnd.randint(min_point,max_point),rnd.randint(min_point, max_point))tree_counter+=1for _ in range(5):t3=Tree(TreeType.peach_tree)t3.render(rnd.randint(age_min,age_max),rnd.randint(min_point,max_point),rnd.randint(min_point, max_point))tree_counter+=1print('trees rendered: {}'.format(tree_counter))print('trees actually created: {}'.format(len(Tree.pool)))t4=Tree(TreeType.cherry_tree)t5=Tree(TreeType.cherry_tree)t6 = Tree(TreeType.apple_tree)print('{} == {}? {}'.format(id(t4),id(t5),id(t4)==id(t5)))print('{} == {}? {}'.format(id(t6), id(t5), id(t6) == id(t5)))完整的代碼(文件flyweight.py)
執(zhí)行上面的程序的結(jié)果:
render a tree of type TreeType.apple_tree and age 28 at (32,67)
render a tree of type TreeType.apple_tree and age 12 at (41,25)
render a tree of type TreeType.apple_tree and age 20 at (54,16)
render a tree of type TreeType.apple_tree and age 29 at (88,50)
render a tree of type TreeType.apple_tree and age 24 at (42,93)
render a tree of type TreeType.apple_tree and age 20 at (38,46)
render a tree of type TreeType.apple_tree and age 9 at (36,89)
render a tree of type TreeType.apple_tree and age 30 at (66,96)
render a tree of type TreeType.apple_tree and age 18 at (87,62)
render a tree of type TreeType.apple_tree and age 12 at (52,2)
render a tree of type TreeType.cherry_tree and age 8 at (49,43)
render a tree of type TreeType.cherry_tree and age 27 at (64,79)
render a tree of type TreeType.cherry_tree and age 15 at (50,31)
render a tree of type TreeType.peach_tree and age 20 at (15,80)
render a tree of type TreeType.peach_tree and age 1 at (60,74)
render a tree of type TreeType.peach_tree and age 6 at (21,31)
render a tree of type TreeType.peach_tree and age 5 at (10,12)
render a tree of type TreeType.peach_tree and age 4 at (53,54)
trees rendered: 18
trees actually created: 3
139957559065120 == 139957559065120? True
139957566714712 == 139957559065120? False
相關(guān)的設(shè)計(jì)模式
- Proxy模式
如果生成實(shí)例的處理需要花費(fèi)較長(zhǎng)時(shí)間,那么使用Flyweight模式可以提高程序的處理速度。
而Proxy模式則是通過(guò)設(shè)置代理提高程序的處理速度。
- Composite模式
有時(shí)可以使用Flyweight模式共享Composite模式中的Leaf角色。
總結(jié)
以上是生活随笔為你收集整理的python 实现 享元模式的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Android数据加密之——Base64
- 下一篇: python box2d 教程_pyth