Python设计模式之享元模式(8)
享元模式(Flyweight Pattern):復用現有的同類對象,改善資源使用
主要用于減少創建對象的數量,以減少內存占用和提高性能。這種類型的設計模式屬于結構型模式,它提供了減少對象數量從而改善應用所需的對象結構的方式。享元模式嘗試重用現有的同類對象,如果未找到匹配的對象,則創建新對象。
OOP編程中容易出現對象創建帶來的性能和內存占用問題,需要滿足以下條件:
- 需要使用大量對象(python里我們可以用__slots__節省內存占用)
- 對象太多難以存儲或解析大量對象。
- 對象識別不是特別重要,共享對象中對象比較會失敗。
經常使用對象池技術來實現共享對象,比如數據庫中經常使用連接池來減少開銷,預先建立一些連接池,每次取一個連接和數據庫交互。
1 介紹
享元模式:運用共享技術有效地支持大量細粒度對象的復用。通過為相似對象引入數據共享來最小化內存使用,提升性能。
意圖:運用共享技術有效地支持大量細粒度的對象。
原理:
- 將具有相同內部狀態的對象存儲在享元池中,享元池中的對象是可以實現共享的
- 需要的時候將對象從享元池中取出,即可實現對象的復用
- 通過向取出的對象注入不同的外部狀態,可以得到一系列相似的對象,而這些對象在內存中實際上只存儲一份
主要解決:在有大量對象時,有可能會造成內存溢出,我們把其中共同的部分抽象出來,如果有相同的業務請求,直接返回在內存中已有的對象,避免重新創建。
何時使用:?
- 系統中有大量對象。
- 這些對象消耗大量內存。
- 這些對象的狀態大部分可以外部化。
- 這些對象可以按照內蘊狀態分為很多組,當把外蘊對象從對象中剔除出來時,每一組對象都可以用一個對象來代替。
- 系統不依賴于這些對象身份,這些對象是不可分辨的。
如何解決:用唯一標識碼判斷,如果在內存中有,則返回這個唯一標識碼所標識的對象。
優點:大大減少對象的創建,降低系統的內存,使效率提高。
缺點:提高了系統的復雜度,需要分離出外部狀態和內部狀態,而且外部狀態具有固有化的性質,不應該隨著內部狀態的變化而變化,否則會造成系統的混亂。
注意事項:?
- 注意劃分外部狀態和內部狀態,否則可能會引起線程安全問題。
- 這些類必須有一個工廠對象加以控制。
-
享元模式,換句話說就是共享對象,在某些對象需要重復創建,且最終只需要得到單一結果的情況下使用。因為此種模式是利用先前創建的已有對象,通過某種規則去判斷當前所需對象是否可以利用原有對象做相應修改后得到想要的效果,如以上教程的實例,創建了20個不同效果的圓,但相同顏色的圓只需要創建一次便可,相同顏色的只需要引用原有對象,改變其坐標值便可。此種模式下,同一顏色的圓雖然位置不同,但其地址都是同一個,所以說此模式適用于結果注重單一結果的情況。
舉一個簡單例子,一個游戲中有不同的英雄角色,同一類型的角色也有不同屬性的英雄,如刺客類型的英雄有很多個,按此種模式設計,利用英雄所屬類型去引用原有同一類型的英雄實例,然后對其相應屬性進行修改,便可得到最終想得到的最新英雄;比如說你創建了第一個刺客型英雄,然后需要設計第二個刺客型英雄,你利用第一個英雄改變屬性得到第二個刺客英雄,最新的刺客英雄是誕生了,但第一個刺客英雄的屬性也隨之變得與第二個相同,這種情況顯然是不可以的。
2 適用場景
享元模式可以避免大量非常相似類的開銷,在程序設計中,有時會生成大量細粒度的類實例來表示數據,如果這些實例除了幾個參數外基本相同,就可以把參數已到實例外面,在方法調用時,把它們傳進來,就可以通過共享大幅度減少單個實例的數目。
一個享元(Flyweight)就是一個包含狀態獨立的不可變(又稱固有的)數據的共享對象。依賴狀態的可變(又稱非固有的)數據不應是享元的一部分,因為每個對象的這種信息都不同,無法共享。如果享元需要非固有的數據,應該由客戶端代碼顯式地提供。
用一個例子可能有助于解釋實際應用場景中如何使用享元模式。假設我們正在設計一個性能關鍵的游戲,例如第一人稱射擊(First-Person Shooter,FPS)游戲。在FPS游戲中,玩家(士兵) 共享一些狀態,如外在表現和行為。例如,在《反恐精英》游戲中,同一團隊(反恐精英或恐怖分子)的所有士兵看起來都是一樣的(外在表現)。同一個游戲中,(兩個團隊的)所有士兵都有一些共同的動作,比如,跳起、低頭等(行為)。這意味著我們可以創建一個享元來包含所有共同的數據。當然,士兵也有許多因人而異的可變數據,這些數據不是享元的一部分,比如,槍支、健康狀況和地理位置等。
主要適用以下場景:
- 系統有大量相似對象。
- 需要緩沖池的場景。
?
3 實現步驟
享元模式包含以下4個角色: Flyweight(抽象享元類) ConcreteFlyweight(具體享元類) UnsharedConcreteFlyweight(非共享具體享元類) FlyweightFactory(享元工廠類)
內部狀態(Intrinsic State):存儲在享元對象內部并且不會隨環境改變而改變的狀態,內部狀態可以共享(例如:字符的內容)
外部狀態(Extrinsic State):隨環境改變而改變的、不可以共享的狀態。享元對象的外部狀態通常由客戶端保存,并在享元對象被創建之后,需要使用的時候再傳入到享元對象內部。一個外部狀態與另一個外部狀態之間是相互獨立的(例如:字符的顏色和大小)
享元池(Flyweight Pool):存儲共享實例對象的地方。
享元模式有多種實現方式:
3.1 使用元類實現享元池,控制實例的創建
具體步驟如下:
步驟一:?定義享元類,實現享元池,達到按需創建對象目的
步驟二:定義具體業務類
步驟三:客戶端傳遞外部狀態,獲取結果
示例代碼:
""" 步驟1:定義享元類,實現按需創建對象 """class FlyweightMeta(type):"""當pool中存在類名和參數完全一致時,返回pool中緩存的instance;不存在時,創建新的instance"""def __new__(mcls, name, parents, dct):""":param mcls: 被繼承的元類,即type類:param name: 衍生類類名:param parents: 衍生類的父類屬性:param dct: 新屬性:return:"""dct['pool'] = dict()return super(FlyweightMeta, mcls).__new__(mcls, name, parents, dct) #復用type.__new__方法def _serialize_params(cls, *args, **kwargs):args_list = list(map(str, args))args_list.extend([str(kwargs), cls.__name__])key = ''.join(args_list)return keydef __call__(self, *args, **kwargs):key = self._serialize_params(*args, **kwargs)pool = getattr(self, 'pool', {})instance = pool.get(key, None)if not instance:instance = super(FlyweightMeta, self).__call__(*args, **kwargs)pool[key] = instancereturn instance""" 步驟2:定義具體業務類,需要使用元類 """ class Book(metaclass=FlyweightMeta):def __init__(self, name, price):# 可變參數name和price,由客戶端提供self.name = nameself.price = pricedef get_book(self):return "{} buy a book: {}, cost: {}".format(id(self), self.name, self.price)if __name__ == "__main__":book_1 = Book("Python基礎", 55)book_2 = Book("Python基礎", 55)book_3 = Book("Python核心基礎", 100)print(book_1.get_book())print(book_2.get_book())print(book_3.get_book())執行結果:
4372629936 buy a book: Python基礎, cost: 55 4372629936 buy a book: Python基礎, cost: 55 4372629880 buy a book: Python核心基礎, cost: 1003.2?在業務基類或工廠類中實現享元模式,共享公用對象
案例一: 當客戶端要創建Tree的一個實例時,會以tree_type參數傳遞樹的種類。 樹的種類用于檢查是否創建過相同種類的樹。如果是,則返回之前創建的對象;否則,將這個新的樹種添加到池中,并返回相應的新對象 方法render()用于在屏幕上渲染一棵樹。注意,享元不知道的所有可變(外部的)信息都需要由客戶端代碼顯式地傳遞。 在當前案例中,每棵樹都用到一個隨機的年齡和一個address。為了讓render()更加有用,有必要確保沒有樹會被渲染到另一個棵之上代碼示例:
import random from enum import EnumTreeType = Enum('TreeType', 'apple_tree cherry_tree peach_tree')""" 步驟1:工廠類實現享元對象池 """ class TreeFactory(object):pool = dict()def __new__(cls, tree_type, *args, **kwargs):obj = cls.pool.get(tree_type, None)if obj is None:obj = super(TreeFactory, cls).__new__(cls)cls.pool[tree_type] = objobj.tree_type = tree_typereturn obj""" 步驟2:業務類 """ class Tree(TreeFactory):def __init__(self, tree_type):self.tree_type = tree_type #共享屬性def render(self, age, address): # age, address為非共享屬性print("{} render a tree of type {} and age {} at {}".format(id(self), self.tree_type, age, address))def main():rnd = random.Random()age_min, age_max = 1, 30app_tree = Tree(TreeType.apple_tree)cherry_tree = Tree(TreeType.cherry_tree)app_tree.render(rnd.randint(age_min, age_max), "南京")app_tree.render(rnd.randint(age_min, age_max), "上海")cherry_tree.render(rnd.randint(age_min, age_max), "南京")cherry_tree.render(rnd.randint(age_min, age_max), "上海")if __name__ == "__main__":main()執行結果:
4365321272 render a tree of type TreeType.apple_tree and age 5 at 南京 4365321272 render a tree of type TreeType.apple_tree and age 13 at 上海 4369574768 render a tree of type TreeType.cherry_tree and age 13 at 南京 4369574768 render a tree of type TreeType.cherry_tree and age 29 at 上海?
4 代碼實踐
享元模式有多種實現方式。
方式一:如上面的例子所示,采用元類實現享元
方式二:在業務基類中實現享元模式,共享公用對象。在子類中實現非共享屬性。
代碼案例如下:
案例1:咖啡店根據客戶喜好,提供2中不同種類咖啡,且不同的咖啡可以支持多種容量size; 不同的size有不同的價格 """ 步驟一:定義產品類 """ class Coffee(object):def __init__(self, name, price):self.name = nameself.price = pricedef serve_coffee(self):print("{} coffee 做好了, 價格: {}".format(self.name, self.price))""" 步驟二:定義產品工廠類,實現享元池 """ class CoffeeFactory(object):"""name為共享對象標識,含有相同name參數的實例會被緩存;name相同,但是price不同的參數,也會返回相同的實例,即price以緩存為準"""coffee_dict = dict()def get_coffee(self, name, price):if not self.coffee_dict.__contains__(name):self.coffee_dict[name] = Coffee(name, price)return self.coffee_dict[name]def get_coffee_count(self):return len(self.coffee_dict)""" 步驟三:客戶端,傳遞非共享參數,獲取coffee對象 """ class Customer(object):def __init__(self, customer_name):self.customer_name = customer_nameself.coffee = CoffeeFactory()def order(self, name, price):instance = self.coffee.get_coffee(name, price)instance.price = price # price為不可共享參數,customer強制修改屬性值instance.serve_coffee()print('給客戶:{} 的coffee: {}好了, 首款:{}'.format(self.customer_name, name, price))print("coffee的個數{}".format(self.coffee.get_coffee_count()))if __name__ == "__main__":coffee_fact = CoffeeFactory()cust_a = Customer("Customer A")cust_b = Customer("Customer B")cust_c = Customer("Customer C")# coffee_1 和 coffee_2 共享相同的實例對象coffee_1 = cust_a.order("Moca", 25)coffee_2 = cust_b.order("Moca", 30)coffee_3 = cust_c.order("cappuccino", 35)執行結果:
Moca coffee 做好了, 價格: 25 給客戶:Customer A 的coffee: Moca好了, 首款:25 coffee的個數1 Moca coffee 做好了, 價格: 30 給客戶:Customer B 的coffee: Moca好了, 首款:30 coffee的個數1 cappuccino coffee 做好了, 價格: 35 給客戶:Customer C 的coffee: cappuccino好了, 首款:35 coffee的個數2?
5?memoization與享元模式
memoization是一種優化技術,使用一個緩存來避免重復計算那些在更早的執行步驟中已經計算好的結果。memoization并不是只能應用于某種特定的編程方式,比如面向對象編程(Object-Oriented Programming,OOP)。在Python中,memoization可以應用于方法和簡單的函數。享元則是一種特定于面向對象編程優化的設計模式,關注的是共享對象數據。
?
6 軟件例子
Exaile音樂播放器(請參考網頁t.cn/RqrjYHQ)使用享元來復用通過相同URL識別的對象 (在這里是指音樂歌曲)。創建一個與已有對象的URL相同的新對象是沒有意義的,所以復用相同的對象來節約資源(請參考網頁t.cn/RqrjQWr)。
Peppy是一個用Python語言實現的類XEmacs編輯器(請參考網頁[t.cn/hbhSda]),它使用享元模式存儲major mode狀態欄的狀態。這是因為除非用戶修改,否則所有狀態欄共享相同的屬性(請參考網頁[t.cn/Rqrjm9y])。這個軟件原作者2014年就放棄了。
?
參考文獻:
https://www.runoob.com/design-pattern/flyweight-pattern.html
https://www.cnblogs.com/welan/p/9128598.html
https://www.jianshu.com/p/2badd38475ea
https://www.cnblogs.com/onepiece-andy/p/python-flyweight-pattern.html
?
總結
以上是生活随笔為你收集整理的Python设计模式之享元模式(8)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: UNIX网络编程---守护进程和inet
- 下一篇: websocket python爬虫_p