享元模式
由于對(duì)象創(chuàng)建的開銷,面向?qū)ο蟮南到y(tǒng)可能會(huì)面臨性能問題粘捎。性能問題通常在資源受限的嵌入式系統(tǒng)中出現(xiàn)薇缅,比如智能手機(jī)和平板電腦。大型復(fù)雜系統(tǒng)中也可能會(huì)出現(xiàn)同樣的問題攒磨,因?yàn)橐谄渲袆?chuàng)建大量對(duì)象(也可能是用戶)泳桦,這些對(duì)象需要同時(shí)并存。
這個(gè)問題之所以會(huì)發(fā)生娩缰,是因?yàn)楫?dāng)我們創(chuàng)建一個(gè)新對(duì)象時(shí)灸撰,需要分配額外的內(nèi)存。雖然虛擬內(nèi)存理論上為我們提供了無限制的內(nèi)存空間,但現(xiàn)實(shí)卻并非如此浮毯。如果一個(gè)系統(tǒng)耗盡了所有的物理內(nèi)存完疫,就會(huì)開始將內(nèi)存頁替換到二級(jí)存儲(chǔ)設(shè)備,通常是硬盤驅(qū)動(dòng)器(Hard Disk Drive亲轨,HDD)趋惨。 在多數(shù)情況下,由于內(nèi)存和硬盤之間的性能差異惦蚊,這是不能接受的器虾。固態(tài)硬盤(Solid State Drive, SSD)的性能一般比硬盤更好,但并非人人都使用SSD蹦锋,SSD并不會(huì)很快全面替代硬盤(請(qǐng)參考 網(wǎng)頁[t.cn/RqrjS0E])兆沙。
除內(nèi)存使用之外,計(jì)算性能也是一個(gè)考慮點(diǎn)莉掂。圖形軟件葛圃,包括計(jì)算機(jī)游戲,應(yīng)該能夠極快地 渲染3D信息(例如憎妙,有成千上萬棵樹的森林或滿是士兵的村莊)库正。如果一個(gè)3D地帶的每個(gè)對(duì)象都 是單獨(dú)創(chuàng)建,未使用數(shù)據(jù)共享厘唾,那么性能將是無法接受的(請(qǐng)參考網(wǎng)頁[t.cn/Rqrj9qa])褥符。
作為軟件工程師,我們應(yīng)該編寫更好的軟件來解決軟件問題抚垃,而不是要求客戶購買更多更好的硬件什燕。享元設(shè)計(jì)模式通過為相似對(duì)象引入數(shù)據(jù)共享來最小化內(nèi)存使用屋吨,提升性能(請(qǐng)參考網(wǎng)頁[t.cn/RqrjNF3])徙瓶。一個(gè)享元(Flyweight)就是一個(gè)包含狀態(tài)獨(dú)立的不可變(又稱固有的)數(shù)據(jù)的共享對(duì)象椰拒。依賴狀態(tài)的可變(又稱非固有的)數(shù)據(jù)不應(yīng)是享元的一部分,因?yàn)槊總€(gè)對(duì)象的這種信息都不同罕伯,無法共享曲伊。如果享元需要非固有的數(shù)據(jù),應(yīng)該由客戶端代碼顯式地提供(請(qǐng)參考[GOF95追他,第219頁]和網(wǎng)頁[t.cn/RqrjOX3])熊昌。
用一個(gè)例子可能有助于解釋實(shí)際應(yīng)用場(chǎng)景中如何使用享元模式。假設(shè)我們正在設(shè)計(jì)一個(gè)性能關(guān)鍵的游戲湿酸,例如第一人稱射擊(First-Person Shooter婿屹,F(xiàn)PS)游戲。在FPS游戲中推溃,玩家(士兵) 共享一些狀態(tài)昂利,如外在表現(xiàn)和行為。例如,在《反恐精英》游戲中蜂奸,同一團(tuán)隊(duì)(反恐精英或恐怖分子)的所有士兵看起來都是一樣的(外在表現(xiàn))犁苏。同一個(gè)游戲中,(兩個(gè)團(tuán)隊(duì)的)所有士兵都有一些共同的動(dòng)作扩所,比如围详,跳起、低頭等(行為)祖屏。這意味著我們可以創(chuàng)建一個(gè)享元來包含所有共同的數(shù)據(jù)助赞。當(dāng)然,士兵也有許多因人而異的可變數(shù)據(jù)袁勺,這些數(shù)據(jù)不是享元的一部分雹食,比如,槍支期丰、健康狀況和地理位置等群叶。
#以下是來自于github的示例
# -*- coding: utf-8 -*-
"""http://codesnipers.com/?q=python-flyweights"""
import weakref
class FlyweightMeta(type):
def __new__(mcs, name, parents, dct):
"""
:param name: class name
:param parents: class parents
:param dct: dict: includes class attributes, class methods,
static methods, etc
:return: new class
"""
# set up instances pool
dct['pool'] = weakref.WeakValueDictionary()
return super(FlyweightMeta, mcs).__new__(mcs, name, parents, dct)
@staticmethod
def _serialize_params(cls, *args, **kwargs):
"""Serialize input parameters to a key.
Simple implementation is just to serialize it as a string
"""
args_list = map(str, args)
args_list.extend([str(kwargs), cls.__name__])
key = ''.join(args_list)
return key
def __call__(cls, *args, **kwargs):
key = FlyweightMeta._serialize_params(cls, *args, **kwargs)
pool = getattr(cls, 'pool', {})
instance = pool.get(key)
if not instance:
instance = super(FlyweightMeta, cls).__call__(*args, **kwargs)
pool[key] = instance
return instance
class Card(object):
"""The object pool. Has builtin reference counting"""
_CardPool = weakref.WeakValueDictionary()
"""Flyweight implementation. If the object exists in the
pool just return it (instead of creating a new one)"""
def __new__(cls, value, suit):
obj = Card._CardPool.get(value + suit)
if not obj:
obj = object.__new__(cls)
Card._CardPool[value + suit] = obj
obj.value, obj.suit = value, suit
return obj
# def __init__(self, value, suit):
# self.value, self.suit = value, suit
def __repr__(self):
return "<Card: %s%s>" % (self.value, self.suit)
class Card2(object):
__metaclass__ = FlyweightMeta
def __init__(self, *args, **kwargs):
# print('Init {}: {}'.format(self.__class__, (args, kwargs)))
pass
if __name__ == '__main__':
import sys
if sys.version_info[0] > 2:
sys.stderr.write("!!! This example is compatible only with Python 2 ATM !!!\n")
raise SystemExit(0)
# comment __new__ and uncomment __init__ to see the difference
c1 = Card('9', 'h')
c2 = Card('9', 'h')
print(c1, c2)
print(c1 == c2, c1 is c2)
print(id(c1), id(c2))
c1.temp = None
c3 = Card('9', 'h')
print(hasattr(c3, 'temp'))
c1 = c2 = c3 = None
c3 = Card('9', 'h')
print(hasattr(c3, 'temp'))
# Tests with metaclass
instances_pool = getattr(Card2, 'pool')
cm1 = Card2('10', 'h', a=1)
cm2 = Card2('10', 'h', a=1)
cm3 = Card2('10', 'h', a=2)
assert (cm1 == cm2) != cm3
assert (cm1 is cm2) is not cm3
assert len(instances_pool) == 2
del cm1
assert len(instances_pool) == 2
del cm2
assert len(instances_pool) == 1
del cm3
assert len(instances_pool) == 0
### OUTPUT ###
# (<Card: 9h>, <Card: 9h>)
# (True, True)
# (31903856, 31903856)
# True
# False
<Card: 9h> <Card: 9h>
True True
4405784136 4405784136
True
False
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-6-f0792005f54c> in <module>()
96
97 # Tests with metaclass
---> 98 instances_pool = getattr(Card2, 'pool')
99 cm1 = Card2('10', 'h', a=1)
100 cm2 = Card2('10', 'h', a=1)
AttributeError: type object 'Card2' has no attribute 'pool'
上面程序的運(yùn)行結(jié)果
root@localhost:~/software# python flyweight.py
(<Card: 9h>, <Card: 9h>)
(True, True)
(140061805283344, 140061805283344)
True
False
"""http://codesnipers.com/?q=python-flyweights"""
import weakref
class Card(object):
"""The object pool. Has builtin reference counting"""
_CardPool = weakref.WeakValueDictionary()
"""Flyweight implementation. If the object exists in the
pool just return it (instead of creating a new one)"""
def __new__(cls, value, suit):
obj = Card._CardPool.get(value + suit, None)
if not obj:
obj = object.__new__(cls)
Card._CardPool[value + suit] = obj
obj.value, obj.suit = value, suit
return obj
# def __init__(self, value, suit):
# self.value, self.suit = value, suit
def __repr__(self):
return "<Card: %s%s>" % (self.value, self.suit)
if __name__ == '__main__':
# comment __new__ and uncomment __init__ to see the difference
c1 = Card('9', 'h')
c2 = Card('9', 'h')
print(c1, c2)
print(c1 == c2)
print(id(c1), id(c2))
<Card: 9h> <Card: 9h>
True
4405761024 4405761024
現(xiàn)實(shí)生活的例子
享元模式是一個(gè)用于優(yōu)化的設(shè)計(jì)模式。因此钝荡,要找一個(gè)合適的現(xiàn)實(shí)生活的例子不太容易街立。我們可以把享元看作現(xiàn)實(shí)生活中的緩存區(qū)。例如埠通,許多書店都有專用的書架來擺放最新和最流行的出版物赎离。這就是一個(gè)緩存區(qū),你可以先在這些專用書架上看看有沒有正在找的書籍植阴,如果沒找到蟹瘾,則可以讓圖書管理員來幫你圾浅。
軟件的例子
Exaile音樂播放器(請(qǐng)參考網(wǎng)頁[t.cn/RqrjYHQ])使用享元來復(fù)用通過相同URL識(shí)別的對(duì)象 (在這里是指音樂歌曲)掠手。創(chuàng)建一個(gè)與已有對(duì)象的URL相同的新對(duì)象是沒有意義的,所以復(fù)用相同的對(duì)象來節(jié)約資源(請(qǐng)參考網(wǎng)頁[http://t.cn/RqrjQWr])狸捕。
Peppy是一個(gè)用Python語言實(shí)現(xiàn)的類XEmacs編輯器(請(qǐng)參考網(wǎng)頁[t.cn/hbhSda])喷鸽,它使用享元模式存儲(chǔ)major mode狀態(tài)欄的狀態(tài)。這是因?yàn)槌怯脩粜薷木呐模駝t所有狀態(tài)欄共享相同的屬性(請(qǐng)參考網(wǎng)頁[t.cn/Rqrjm9y])做祝。這個(gè)軟件原作者2014年就放棄了。
應(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(那些在客戶端代碼看來不同的對(duì)象,最終具有相同的ID)眯杏。
實(shí)現(xiàn)
由于之前已提到樹的例子夜焦,那么就來看看如何實(shí)現(xiàn)它。在這個(gè)例子中岂贩,我們將構(gòu)造一小片水果樹的森林茫经,小到能確保在單個(gè)終端頁面中閱讀整個(gè)輸出。然而萎津,無論你構(gòu)造的森林有多大卸伞,內(nèi)存分配都保持相同。下面這個(gè)Enum類型變量描述三種不同種類的水果樹锉屈。
TreeType = Enum('TreeType', 'apple_tree cherry_tree peach_tree')
現(xiàn)實(shí)生活中的例子
在深入代碼之前荤傲,我們稍稍解釋一下memoization與享元模式之間的區(qū)別。memoization是一種優(yōu)化技術(shù)颈渊,使用一個(gè)緩存來避免重復(fù)計(jì)算那些在更早的執(zhí)行步驟中已經(jīng)計(jì)算好的結(jié)果遂黍。memoization并不是只能應(yīng)用于某種特定的編程方式,比如面向?qū)ο缶幊?Object-Oriented Programming俊嗽,OOP)雾家。在Python中,memoization可以應(yīng)用于方法和簡單的函數(shù)绍豁。享元?jiǎng)t是一種特定于面向?qū)ο缶幊虄?yōu)化的設(shè)計(jì)模式芯咧,關(guān)注的是共享對(duì)象數(shù)據(jù)。
在Python中竹揍,享元可以以多種方式實(shí)現(xiàn)敬飒,但我發(fā)現(xiàn)這個(gè)例子中展示的實(shí)現(xiàn)非常簡潔。pool變量是一個(gè)對(duì)象池(換句話說芬位,是我們的緩存)无拗。注意:pool是一個(gè)類屬性(類的所有實(shí)例共享的一個(gè)變量,請(qǐng)參考網(wǎng)頁[t.cn/zHwpgFe])昧碉。使用特殊方法new(這個(gè)方法在init之前被調(diào)用)英染,我們把Tree類變換成一個(gè)元類阴孟,元類支持自引用。這意味著cls引用的是Tree類(請(qǐng)參考[Lott14税迷,第99頁])永丝。當(dāng)客戶端要?jiǎng)?chuàng)建Tree的一個(gè)實(shí)例時(shí),會(huì)以tree_type參數(shù)傳遞樹的種類箭养。樹的種類用于檢查是否創(chuàng)建過相同種類的樹慕嚷。如果是,則返回之前創(chuàng)建的對(duì)象毕泌;否則喝检,將這個(gè)新的樹種添加到池中,并返回相應(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] = obj
obj.tree_type = tree_type
return obj
方法render()用于在屏幕上渲染一棵樹。注意愿题,享元不知道的所有可變(外部的)信息都需要由客戶端代碼顯式地傳遞损俭。在當(dāng)前案例中,每棵樹都用到一個(gè)隨機(jī)的年齡和一個(gè)x, y形式的位置潘酗。為了讓render()更加有用杆兵,有必要確保沒有樹會(huì)被渲染到另一個(gè)棵之上。你可以考慮把這個(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ù)展示了我們可以如何使用享元模式日裙。一棵樹的年齡是1到30年之間的一個(gè)隨機(jī)值。坐標(biāo)使用1到100之間的隨機(jī)值惰蜜。雖然渲染了18棵樹昂拂,但僅分配了3棵樹的內(nèi)存。輸出的最后一行證明當(dāng)使用享元時(shí)蝎抽,我們不能依賴對(duì)象的ID政钟。函數(shù)id()會(huì)返回對(duì)象的內(nèi)存地址路克。Python規(guī)范并沒有要求id()返回對(duì)象的內(nèi)存地址樟结,只是要求id()為每個(gè)對(duì)象返回一個(gè)唯一性ID,不過CPython(Python的官方實(shí)現(xiàn))正好使用對(duì)象的內(nèi)存地址作為對(duì)象唯一性ID精算。在我們的例子中瓢宦,即使兩個(gè)對(duì)象看起來不相同,但是如果它們屬于同一個(gè)享元家族(在這里灰羽,家族由tree_type定義)驮履,那么它們實(shí)際上有相同的ID鱼辙。當(dāng)然,不同ID的比較仍然可用于不同家族的對(duì)象玫镐,但這僅在客戶端知道實(shí)現(xiàn)細(xì)節(jié)的情況下才可行(通常并非如此)倒戏。
def main():
rnd = random.Random()
age_min, age_max = 1, 30 # 單位為年
min_point, max_point = 0, 100
tree_counter = 0
for _ 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 += 1
for _ 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 += 1
for _ 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 += 1
print('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(t5), id(t6), id(t5) == id(t6)))
下面完整的代碼清單(文件flyweight.py)將給出享元模式如何實(shí)現(xiàn)及使用的完整描述。
import random
from enum import Enum
TreeType = Enum('TreeType', 'apple_tree cherry_tree peach_tree')
class Tree:
pool = dict()
def __new__(cls, tree_type):
obj = cls.pool.get(tree_type, None)
if not obj:
obj = object.__new__(cls)
cls.pool[tree_type] = obj
obj.tree_type = tree_type
return obj
def render(self, age, x, y):
print('render a tree of type {} and age {} at ({}, {})'.format(self.tree_type, age, x, y))
def main():
rnd = random.Random()
age_min, age_max = 1, 30 # 單位為年
min_point, max_point = 0, 100
tree_counter = 0
for _ 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 += 1
for _ 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 += 1
for _ 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 += 1
print('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(t5), id(t6), id(t5) == id(t6)))
main()
render a tree of type TreeType.apple_tree and age 29 at (13, 7)
render a tree of type TreeType.apple_tree and age 21 at (40, 66)
render a tree of type TreeType.apple_tree and age 3 at (53, 52)
render a tree of type TreeType.apple_tree and age 19 at (100, 84)
render a tree of type TreeType.apple_tree and age 11 at (57, 56)
render a tree of type TreeType.apple_tree and age 22 at (20, 37)
render a tree of type TreeType.apple_tree and age 11 at (7, 16)
render a tree of type TreeType.apple_tree and age 3 at (10, 18)
render a tree of type TreeType.apple_tree and age 17 at (85, 75)
render a tree of type TreeType.apple_tree and age 4 at (97, 34)
render a tree of type TreeType.cherry_tree and age 17 at (72, 29)
render a tree of type TreeType.cherry_tree and age 10 at (26, 79)
render a tree of type TreeType.cherry_tree and age 1 at (85, 56)
render a tree of type TreeType.peach_tree and age 6 at (44, 71)
render a tree of type TreeType.peach_tree and age 4 at (42, 50)
render a tree of type TreeType.peach_tree and age 27 at (85, 24)
render a tree of type TreeType.peach_tree and age 27 at (51, 22)
render a tree of type TreeType.peach_tree and age 23 at (37, 55)
trees rendered: 18
trees actually created: 3
4405186288 == 4405186288? True
4405186288 == 4405186456? False
執(zhí)行上面的示例程序會(huì)顯示被渲染對(duì)象的類型恐似、隨機(jī)年齡以及坐標(biāo)杜跷,還有相同/不同家族享元對(duì)象ID的比較結(jié)果。你在執(zhí)行這個(gè)程序時(shí)別指望能看到與下面相同的輸出矫夷,因?yàn)槟挲g和坐標(biāo)是隨機(jī)的葛闷,對(duì)象ID也依賴內(nèi)存映射。
如果你想更多地練習(xí)一下享元模式双藕,可以嘗試實(shí)現(xiàn)本章提到的FPS士兵淑趾。思考一下哪些數(shù)據(jù)應(yīng)該是享元的一部分(不可變的、內(nèi)部的)忧陪,哪些數(shù)據(jù)不應(yīng)該是(可變的扣泊、外部的)。
小結(jié)
本章中嘶摊,我們學(xué)習(xí)了享元模式旷赖。在我們想要優(yōu)化內(nèi)存使用提高應(yīng)用性能之時(shí),可以使用享元更卒。在所有內(nèi)存受限(想一想嵌入式系統(tǒng))或關(guān)注性能的系統(tǒng)(比如圖形軟件和電子游戲)中等孵,這一點(diǎn)相當(dāng)重要□蹇眨基于GTK+的Exaile音樂播放器使用享元來避免對(duì)象復(fù)制俯萌,Peppy文本編輯器則使用享元來共享狀態(tài)欄的屬性。
一般來說上枕,在應(yīng)用需要?jiǎng)?chuàng)建大量的計(jì)算代價(jià)大但共享許多屬性的對(duì)象時(shí)咐熙,可以使用享元。重點(diǎn)在于將不可變(可共享)的屬性與可變的屬性區(qū)分開辨萍。我們實(shí)現(xiàn)了一個(gè)樹渲染器棋恼,支持三種不同的樹家族。通過顯式地向render()方法提供可變的年齡和x锈玉,y屬性爪飘,我們成功地僅創(chuàng)建了3個(gè)不同的對(duì)象,而不是18個(gè)拉背。雖然那看起來似乎沒什么了不起师崎,但是想象一下,如果是2000棵樹而不是18棵樹椅棺,那又會(huì)怎樣呢?