python 实现 享元模式
本文的目錄地址
本文的代碼地址
由于對象創建的開銷,面向對象的系統可能會面臨性能問題。性能問題通常在資源受限的嵌入式系統中出現,比如智能手機和平板電腦。大型復雜系統中也可能會出現同樣的問題,因為要在其中創建大量對象(用戶),這些對象需要同時并存。
這個問題之所以會發生,是因為當我們創建一個新對象時,需要分配額外的內存。雖然虛擬內存理論上為我們提供了無限制的內存空間,但現實卻并非如此。如果一個系統耗盡了所有的物理內存,就會開始將內存頁替換到二級存儲設備,通常是硬盤驅動器(Hard Disk Drive,HDD)。在多數情況下,由于內存和硬盤之間的性能差異,這是不能接受的。固態硬盤(Solid State Drive,SSD)的性能一般比硬盤更好,但并非人人都使用SSD,SSD并不會很快全面替代硬盤。
除內存使用之外,計算性能也是一個考慮點。圖形軟件,包括計算機游戲,應該能夠極快地渲染3D信息(例如,有成千上萬顆樹的森林或滿是士兵的村莊)。如果一個3D地帶的每個對象都是單獨創建,未使用數據共享,那么性能將是無法接受的。
作為軟件工程師,我們應該編寫更好的軟件來解決軟件問題,而不是要求客戶購買更多更好的硬件。享元設計模式通過為相似對象引入數據共享來最小化內存使用,提升性能。一個享元(Flyweight)就是一個包含狀態獨立的不可變(又稱固有的)數據的共享對象。依賴狀態的可變(又稱非固有的)數據不應是享元的一部分,因為每個對象的這種信息都不同,無法共享。如果享元需要非固有的數據,應該由客戶端代碼顯示地提供。
用一個例子可能有助于解釋實際場景中如何使用享元模式。假設我們正在設計一個性能關鍵的游戲,例如第一人稱射擊(First-Person Shooter,FPS)游戲。在FPS游戲中,玩家(士兵)共享一些狀態,如外在表現和行為。例如,在《反恐精英》游戲中,同一團隊(反恐精英或恐怖分子)的所有士兵看起來都是一樣的(外在表現)。同一個游戲中,(兩個團隊的)所有士兵都有一些共同的動作,比如,跳起、低頭等(行為)。這意味著我們可以創建一個享元來包含所有共同的數據。當然,士兵也有許多因人而異的可變數據,這些數據不是享元的一部分,比如,槍支、健康狀況和地理位置等。
現實生活中的例子
享元模式(Flyweight pattern)是一個用于優化的設計模式。因此,要找一個合適的現實生活的例子不太容易。我們可以把享元看做現實生活中的緩存區。例如,許多書店都有專用的書架來擺放最新和流行的出版物。這就是一個緩存區,你可以先在這些專用書架上看看有沒有正在找的書籍,如果沒找到,則可以讓圖書管理員來幫你。
軟件的例子
Exaile音樂播放器使用享元來復用通過相同URL識別的對象(在這里是指音樂歌曲)。創建一個與已有對象的URL相同的新對象是沒有意義的,所以復用相同的對象來節約資源。
Peppy是一個用Python語言實現的類XEmacs編輯器,它使用享元模式存儲major mode狀態欄的狀態。這是因為除非用戶修改,否則所有狀態欄共享相同的屬性。
應用案例
享元旨在優化性能和內存使用。所有嵌入式系統(手機、平板電腦、游戲終端和微控制器等)和性能關鍵的應用(游戲、3D圖形處理和實時系統等)都能從其獲益。
若想要享元模式有效,需要滿足GoF的《設計模式》一書羅列的以下幾個條件。
- 應用需要使用大量的對象
- 對象太多,存儲/渲染它們的代價太大。一旦移除對象中的可變狀態(因為在需要之時,應該由客戶端代碼顯示地傳遞給享元),多組不同的對象可被相對更少的共享對象所替代。
- 對象ID對于應用不重要。對象共享會造成ID比較的失敗,所以不能依賴對象ID(那些在客戶端代碼看來不同的對象,最終具有相同的ID)。
實現
由于之前已提到樹的例子,那么就來看看如何實現它。在這個例子中,我們將構造一小片水果樹的森林,小到能確保在單個終端頁面中閱讀整個輸出。然而,無論你構造的森林有多大,內存分配都保持相同。下面這個Enum類型變量描述三種不同種類的水果樹。
TreeType=Enum('TreeType','apple_tree cherry_tree peach_tree')在深入代碼之前,我們稍稍解釋一下memoization與享元模式之間的區別。memoization是一種優化技術,使用一個緩存來避免重復計算那些在更早的執行步驟中已經計算好的結果。memoization并不是只能應用于某種特定的編程方式,比如面向對象編程(Object-Oriented Programming,OOP)。在Python中,memoization可以應用于方法和簡單的函數。享元則是一種特定于面向對象編程優化的設計模式,關注的是共享對象數據。
在Python中,享元可以以多種方式實現。pool變量是一個對象池(換句話說,是我們的緩存)。注意:pool是一個類屬性(類的所有實例共享的一個變量)。使用特殊方法__new__(這個方法在__init__之前被調用),我們把Tree類變換成一個元類,元類支持自引用。這意味著cls引用的是Tree類。當客戶端要創建Tree的一個實例時,會以tree_type參數傳遞樹的種類。樹的種類用于檢查是否創建過相同種類的樹。如果是,則返回之前創建的對象;否則,將這個新的樹種添加到池中,并返回相應的新對象,如下所示。
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()用于在屏幕上渲染一棵樹。注意,享元不知道的所有可變(外部的)信息都需要客戶端代碼顯示地傳遞。在當前案例中,每棵樹都用到一個隨機的年齡和一個x,y形式的位置。為了讓render()更加有用,有必要確保沒有樹會被渲染到另一顆之上。你可以考慮把這個作為練習。如果你想讓渲染更加有趣,可以使用一個圖形工具包,比如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()函數展示了我們可以如何使用享元模式。一棵樹的年齡是1-30年之間的一個隨機數。坐標使用1-100之間的隨機數。雖然渲染了18棵樹,但僅分配了3顆樹的內存。輸出的最后一行證明當使用享元時,我們不能依賴對象的ID。函數id()會返回對象的內存地址。Python規范并沒有要求id()返回對象的內存地址,只是要求id()為每個對象返回一個唯一性ID,不過CPython(Python的官方實現)正好使用對象的內存地址作為對象唯一性ID。在我們的例子中,即使兩個對象看起來不相同,但是如果它們屬于同一個享元家族(在這里,家族由tree_type定義),那么它們實際上有相同的ID。當然,不同ID的比較仍然可用于不同家族的對象,但這僅在客戶端知道實現細節的情況下才可行(通常并非如此)。
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)
執行上面的程序的結果:
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
相關的設計模式
- Proxy模式
如果生成實例的處理需要花費較長時間,那么使用Flyweight模式可以提高程序的處理速度。
而Proxy模式則是通過設置代理提高程序的處理速度。
- Composite模式
有時可以使用Flyweight模式共享Composite模式中的Leaf角色。
總結
以上是生活随笔為你收集整理的python 实现 享元模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android数据加密之——Base64
- 下一篇: 达尔优 绿野EK861蓝牙键盘说明书