通俗 Python 設計模式——享元模式

享元模式是一種用於解決資源和性能壓力時會使用到的設計模式,它的核心思想是通過引入數據共享來提升性能

我們知道程序開發的重點是對現實世界的抽象,那麼相似的對象必然有某些相同的屬性或行為。比如遊戲中,每個角色的均可以做一些相同的動作,同樣類型的角色有更多相同的動作。那麼,出於優化性能減小資源開銷的目的,在應用需要創建大量的計算代價大但共享許多屬性的對象時,可以使用享元。重點在於將不可變(可共享)的屬性與可變的屬性區分開。

下面用書中的實際的代碼示例來說明。

假設場景,我們需要模擬一片樹林,其中有不同年齡的蘋果樹、梨樹和櫻桃樹分布在不同的位置。我們先定義Tree這個類,並設置TreeType為幾種樹木的枚舉。

from enum import Enum nTreeType = Enum(TreeType, apple_tree cherry_tree peach_tree)nnclass Tree(object): n def __init__(self, tree_type, age, x, y):n self.tree_type = tree_typen self.age = agen self.x = xn self.y = ynn def render(self):n print(類型 {} 年齡 {} 位置 ({}, {}).format(self.tree_type, self.age, self.x, self.y))n

如此我們就成功創建了一個Tree的類,其中的tree_type用於標識每個實例到底是哪一種樹。

但是,這樣一來,我們就會發現,當樹林中樹木非常多的時候,我們的樹對象的數量將會急劇膨脹,在數量很大或者資源很緊張的時候是不可接受的。所以我們需要考慮使用享元模式來提取通用數據以節約資源。

這裡我們要明確兩個知識點,第一是Python中類屬性與實例屬性的區別,第二是def __new__方法與def __init__方法的區別。具體知識點不是本文討論重點,請各位自行查閱資料學習,這裡直接給出使用這兩個知識點實現享元模式以解決之前方案問題的辦法。

class Tree(object): n pool = dict()nn def render(self, age, x, y):n print(類型 {} 年齡 {} 位置 ({}, {}).format(self.tree_type, age, x, y))nn def __new__(cls, tree_type):n t = cls.pool.get(tree_type, None)n if t:n passn else:n t = object.__new__(cls)n cls.pool[tree_type] = tn t.tree_type = tree_typen return tn

將Tree類如此修改一番,我們就實現了享元模式。簡單做一個講解。

我們為Tree類型提供了一個類屬性pool代表這個類的對象池——可以理解為一個對象數據的緩存,而def __new__方法,將Tree類改造為一個元類,實現了引用自身的目的。於是,在每次創建新的對象時先到pool中檢查是否有該類型的的對象存在,如果存在就直接返回之前創建好的那個對象,如果不存在則在pool中添加這個新的對象,如此一來就實現了對象的復用。這裡要注意的是,我們使用了__new__方法後,移除了__init__方法,並把age,x和y等可變數據都放到了其他地方,就是為了保留最通用的數據,這也是實現享元模式的核心

下面我們測試一下:

def main(): n import randomn rnd = random.Random()n age_min, age_max = 1, 30 # 單位為年n min_point, max_point = 0, 100n tree_counter = 0nn for _ in range(10):n t1 = Tree(TreeType.apple_tree)n t1.render(rnd.randint(age_min, age_max),n rnd.randint(min_point, max_point),n rnd.randint(min_point, max_point))n tree_counter += 1nn for _ in range(3):n t2 = Tree(TreeType.cherry_tree)n t2.render(rnd.randint(age_min, age_max),n rnd.randint(min_point, max_point),n rnd.randint(min_point, max_point))n tree_counter += 1nn for _ in range(5):n t3 = Tree(TreeType.peach_tree)n t3.render(rnd.randint(age_min, age_max),n rnd.randint(min_point, max_point),n rnd.randint(min_point, max_point))n tree_counter += 1nn print(生成並渲染的樹木: {}棵.format(tree_counter))n print(創建的樹木對象: {}個.format(len(Tree.pool)))nn t4 = Tree(TreeType.cherry_tree)n t5 = Tree(TreeType.cherry_tree)n t6 = Tree(TreeType.apple_tree)n print({} == {}? {}.format(id(t4), id(t5), id(t4) == id(t5)))n print({} == {}? {}.format(id(t5), id(t6), id(t5) == id(t6)))n t4.render(4, 4, 4)n t5.render(5, 5, 5)n t6.render(6, 6, 6)n

結果輸出如下:

$ python flyweight.pyn類型 TreeType.apple_tree 年齡 6 位置 (98, 11)n類型 TreeType.apple_tree 年齡 20 位置 (90, 43)n類型 TreeType.apple_tree 年齡 10 位置 (100, 7)n類型 TreeType.apple_tree 年齡 6 位置 (65, 40)n類型 TreeType.apple_tree 年齡 11 位置 (56, 99)n類型 TreeType.apple_tree 年齡 21 位置 (15, 33)n類型 TreeType.apple_tree 年齡 26 位置 (9, 9)n類型 TreeType.apple_tree 年齡 6 位置 (26, 94)n類型 TreeType.apple_tree 年齡 26 位置 (89, 96)n類型 TreeType.apple_tree 年齡 13 位置 (50, 26)n類型 TreeType.cherry_tree 年齡 28 位置 (17, 37)n類型 TreeType.cherry_tree 年齡 6 位置 (27, 47)n類型 TreeType.cherry_tree 年齡 29 位置 (31, 15)n類型 TreeType.peach_tree 年齡 23 位置 (63, 99)n類型 TreeType.peach_tree 年齡 5 位置 (9, 76)n類型 TreeType.peach_tree 年齡 30 位置 (58, 48)n類型 TreeType.peach_tree 年齡 14 位置 (60, 35)n類型 TreeType.peach_tree 年齡 3 位置 (64, 17)n生成並渲染的樹木: 18棵n創建的樹木對象: 3個n522952945848 == 522952945848? True n522952945848 == 522954153824? False n類型 TreeType.cherry_tree 年齡 4 位置 (4, 4)n類型 TreeType.cherry_tree 年齡 5 位置 (5, 5)n類型 TreeType.apple_tree 年齡 6 位置 (6, 6)n

可以看到,最後的內存單元編號證明,同樣類型的樹木對象共享了同一個樹木類型數據。但會改變的部分數據——年齡,位置——又互不影響,在樹木數量超大時,將有效提升性能。

最後要注意:享元模式不能依賴對象的id。在上面的測試實例中可以看到,因為使用了享元模式,所以本來是不同的兩棵樹,在做id對比時,卻是同一個對象,這是因為當前一個對象動作完成後,後一個對象就覆蓋了前一個對象。如果不注意,可能會在開發中埋下很大的隱患。要測試也很簡單,我們將Tree類的代碼改成下面的形式:

from enum import EnumnnnTreeType = Enum(TreeType, apple_tree cherry_tree peach_tree)nnclass Tree(object): n tree_type = n pool = dict()nn def __init__(self, tree_type, age):n self.age = agenn def render(self, x, y):n print(類型 {} 年齡 {} 位置 ({}, {}).format(self.tree_type, self.age, x, y))nn def __new__(cls, tree_type, age):n t = cls.pool.get(tree_type, None)n if t:n passn else:n t = object.__new__(cls)n cls.pool[tree_type] = tn t.tree_type = tree_typen return tnnndef main(): n import randomn rnd = random.Random()n age_min, age_max = 1, 30 # 單位為年n min_point, max_point = 0, 100n tree_counter = 0nn for _ in range(10):n t1 = Tree(TreeType.apple_tree,n rnd.randint(age_min, age_max))n t1.render(rnd.randint(min_point, max_point),n rnd.randint(min_point, max_point))n tree_counter += 1nn for _ in range(3):n t2 = Tree(TreeType.cherry_tree,n rnd.randint(age_min, age_max))n t2.render(rnd.randint(min_point, max_point),n rnd.randint(min_point, max_point))n tree_counter += 1nn for _ in range(5):n t3 = Tree(TreeType.peach_tree,n rnd.randint(age_min, age_max))n t3.render(rnd.randint(min_point, max_point),n rnd.randint(min_point, max_point))n tree_counter += 1nn print(生成並渲染的樹木: {}棵.format(tree_counter))n print(創建的樹木對象: {}個.format(len(Tree.pool)))nn t4 = Tree(TreeType.cherry_tree,n 4)n t5 = Tree(TreeType.cherry_tree,n 5)n t6 = Tree(TreeType.apple_tree,n 6)n print({} == {}? {}.format(id(t4), id(t5), id(t4) == id(t5)))n print({} == {}? {}.format(id(t5), id(t6), id(t5) == id(t6)))n t4.render(4, 4)n t5.render(5, 5)n t6.render(6, 6)nnif __name__ == __main__: n main()n

這裡將age放到了__init__方法中,按照設想,他應該成為對象自身的屬性,即每個對象均不同,那麼我們跑一跑代碼:

$ python flyweight.pyn類型 TreeType.apple_tree 年齡 19 位置 (57, 8)n類型 TreeType.apple_tree 年齡 26 位置 (21, 16)n類型 TreeType.apple_tree 年齡 30 位置 (33, 72)n類型 TreeType.apple_tree 年齡 18 位置 (98, 79)n類型 TreeType.apple_tree 年齡 9 位置 (90, 15)n類型 TreeType.apple_tree 年齡 25 位置 (17, 13)n類型 TreeType.apple_tree 年齡 15 位置 (100, 86)n類型 TreeType.apple_tree 年齡 8 位置 (45, 4)n類型 TreeType.apple_tree 年齡 19 位置 (11, 6)n類型 TreeType.apple_tree 年齡 18 位置 (18, 46)n類型 TreeType.cherry_tree 年齡 25 位置 (42, 68)n類型 TreeType.cherry_tree 年齡 9 位置 (8, 50)n類型 TreeType.cherry_tree 年齡 1 位置 (60, 28)n類型 TreeType.peach_tree 年齡 2 位置 (42, 73)n類型 TreeType.peach_tree 年齡 7 位置 (87, 59)n類型 TreeType.peach_tree 年齡 19 位置 (26, 23)n類型 TreeType.peach_tree 年齡 21 位置 (3, 22)n類型 TreeType.peach_tree 年齡 16 位置 (43, 73)n生成並渲染的樹木: 18棵n創建的樹木對象: 3個n332890664464 == 332890664464? True n332890664464 == 332889074432? False n類型 TreeType.cherry_tree 年齡 5 位置 (4, 4)n類型 TreeType.cherry_tree 年齡 5 位置 (5, 5)n類型 TreeType.apple_tree 年齡 6 位置 (6, 6)n

最後幾行,在(4, 4)位置的t4櫻桃樹年齡本該是4,卻變成了5,即位置在(5, 5)的t5櫻桃樹的年齡,說明t4中的共享部分數據其實已經被t5所覆蓋。

推薦閱讀:

為什麼Python類成員的調用和聲明必須有"this"?
python及numpy,pandas易混淆的點
python2.7的sort函數默認採用什麼排序演算法,適用於怎樣的數列的排序?
爬蟲入門到精通-網頁的解析(xpath)
*吧上有海外留學生問全部用遞歸求第N個質數,不能用循環

TAG:Python | 设计模式 |