Python 设计模式之享元模式


享元模式( flyweight pattern)属于结构型设计模式,主要用于解决系统中大量创建同一个类的实例时导致的内存激增的问题,它的解决思路是将类的实例属性拆分成外部属性和内部属性。

  • 外部属性:会被外部修改的属性
  • 内部属性:实例创建之后就不会变更的属性

从一个 MP3 案例谈起

现编案例,如有不恰当可以指正

案例
假设我们现在有一个系统用于管理每个人的 MP3 设备,这些 MP3 有不同的颜色、牌子、型号…还有每个人在设备里的个性化配置、自己导入的音乐。

代码实现
系统的早期实现版本如下:

import tracemalloc
from collections import namedtuple

def parse_comma_data(raw_data:str, char=","):
    lines = raw_data.split("\n")
    return (l.split(char) for l in lines)

class MP3:
    def __init__(self, brand, model, memory_size, color, settings, music_list=None):
        self.brand = brand
        self.model = model
        self.memory_size = memory_size
        self.color = color
        self.settings = settings
        self.music_list = music_list
        self.equipment_parameters = bytes(10000000)
        self.system = bytes(10000000)
        
    def __str__(self):
        return f"{self.model} {self.color} {self.memory_size} with id:{id(self)}"

    def __repr__(self):
        return self.__str__()
    
    def add_music(self, new_music:str):
        if self.music_list is None:
            self.music_list = []
            
        self.music_list.append(new_music)
    
# 系统需要管理的 MP3 设备
mp3_raw_list = """john,sony,S1001,4G,red
cindy,sony,S1001,4G,red
simon,sony,S1001,4G,red
lucy,sony,S1001,4G,red
babala,sony,S1002,4G,red
tom,sony,S1002,4G,red
dikaer,sony,S1005,16G,red"""

def main():
    mp3_devices = {}
    for data in parse_comma_data(mp3_raw_list):
        name,brand,model,memory_size,color = data
        mp3_devices[name] = MP3(
            brand, model, memory_size, color, settings=name
        )
    
    return mp3_devices
    
if __name__ == "__main__":
    tracemalloc.start()
    mp3_list = main()
    print("内存占用:", tracemalloc.get_traced_memory()[0])
    tracemalloc.stop()
    for name, mp3 in mp3_list.items():
        print(name, mp3)

上面这段代码将mp3_raw_list 中记录的每个独立的 MP3 导入系统中管理,系统可以根据指定用户调用他们对应的 mp3 对象 (mp3_list[name])。

值得注意的是 MP3 class 中有两个属性比较大,比较占内存空间:

        self.equipment_parameters = bytes(10000000)
        self.system = bytes(10000000)

同时,我们调用了 tracemalloc 库来监测内存的使用情况,并在代码最后打印每个设备的 id, 下面是它的控制台输出

内存占用: 140004589
john S1001 red 4G with id:1658049569424
cindy S1001 red 4G with id:1658049567568
simon S1001 red 4G with id:1658049573200
lucy S1001 red 4G with id:1658049567504
babala S1002 red 4G with id:1658049561936
tom S1002 red 4G with id:1658049561808
dikaer S1005 red 16G with id:1658049562576

从这段输出我们可以看到三个点:

  1. 这些 mp3 设备有一些是同一款产品,比如 john、cindy、simon、lucy 他们四个人都是 sony,S1001,4G,red, 但也注意到他们每个人的设置都不一样,这点可以看 mp3.settings 这个属性都不同就知道
  2. 这些 mp3 对象在系统中都是独立的,这点可以从它们的 id 都不同看出来
  3. 每个 mp3 对象的内存开销大部分消耗在设备基础属性(equipment_parameters)和操作系统(system), 每增加一个在线用户就需要增加 20000000 字节的空间消耗:
     # self.equipment_parameters = bytes(10000000)
     # self.system = bytes(10000000)
    上面有 7 个设备,则消耗:
    10000000 * 2 * 7 = 140000000 
    这个数值接近上面的控制台输出 140004589
    

flyweight 模式解决

flyweight pattern 的组件

首先我们来看下 flyweight 模式有哪些关键组件,然后再按些组件的功能定义来实现代码。

  • Flyweight : 包含多个对象共享的固有状态,同一个 flyweight 对象可在不同的上下文中使用。它存储内在状态,并从上下文中接收外在状态。
  • Context: 保存所有原始对象独有的外在状态。当与一个 flyweight 对象配对时,它代表了原始对象的全部状态。
  • Flyweight Factory: 管理现有的 flyweight 实例池,处理 flyweight 实例的创建和重用。Client 通过与 factory 交互来获取 flyweight 实例,并传递 内部属性 以进行检索或创建。
  • Client: 计算或存储 flyweight 对象的外部属性。 它将 flyweight 视为模板对象,并在运行时通过向其方法中传递上下文数据对其进行配置。

拆解定义与逐步实现

这段定义显然不够清晰,那我们配合着图形与案例看它们的关系:

对于我们的 MP3 实例而言,它应该是由内在状态(属性)和外在状态(属性)组成

MP3 对象属性
intrinsic 内部属性
extrinsic 外部属性
model,brand,color,memory_size
settings, music_list

显然除了self.settingsself.music_list 两个属性是用户在拥有设备后可以更改的属性,其他的属性无法修改(这里指的是现实商品层面的无法修改,我们并没有在代码实现上阻止调用者修改这些实例属性)

根据上面的定义知道两点:

  1. flyweight 对象具有 MP3 实例的内部属性
  2. context 对象具有 MP3 实例的内部属性外部属性

根据这两点,我们继续演进上面的关系图:

MP3 obj
intrinsic 内部属性
extrinsic 外部属性
flyweight obj
context obj

从这里我们可以看到 MP3 objcontext obj 是指代一种东西,即一个完整的 MP3 对象,实现的过程中命名为 context 还是 MP3 都是一样的,区别在于

mp3_obj = 内部属性 + 外部属性
context_obj = flyweight_obj + 外部属性

现在我们可以完成代码中 flyweightcontext 部分的实现了:

# 此时 MP3 是一个 flyweight 对象
class MP3:
    def __init__(self, brand, model, memory_size, color):
        self.brand = brand
        self.model = model
        self.memory_size = memory_size
        self.color = color
        
        # 比较占内存空间的变量
        self.equipment_parameters = bytes(1000000)
        self.system = bytes(1000000)

        
    def __str__(self):
        return f"{self.model} {self.color} {self.memory_size} with id:{id(self)}"

    def __repr__(self):
        return self.__str__()
        

# context 对象,或者说具体的 mp3 对象,它具备了其内部属性和外部属性
class PersonalMP3:
    def __init__(self, brand,model,memory_size,color, settings="default setting", music_list=None):
        self.mp3 = MP3(brand,model,memory_size,color)
        self.settings = settings
        self.music_list = music_list
        
    def add_music(self, new_music:str):
        if self.music_list is None:
            self.music_list = []
            
        self.music_list.append(new_music)

上面这段代码我们让 MP3 class 只保留内部属性;而 PersonalMP3 class 则由这个 MP3 class 和外部属性组成。

需要注意的是,此时我们的 PersonalMP3.mp3 并没有复用,它根据入参实例化一个对象!

现在回过头来看 flyweight factory 的定义:

  1. 管理现有的 flyweight 实例池
  2. 传递 内部属性 以进行检索或创建

基于这段定义,我们需要继续修改上面的关系图:
在这里插入图片描述
根据第二点(传递 内部属性 以进行检索或创建),flyweight factory 返回实例的逻辑图应该是:
在这里插入图片描述
再次修改代码,增加 flyweight factory , 并修改上面 PersonalMP3 属性中 MP3 实例的获取逻辑。

from collections import namedtuple

Device = namedtuple("Device", ["brand", "model", "memory_size", "color"])

class MP3:
	# 省略
	...

# flyweight factory 用于提供 flyweight obj
class MP3Factory:
    cache = {}
    
    @classmethod
    def get_mp3(cls, device: Device):
        if device not in cls.cache:
            cls.cache[device] = MP3(
                brand=device.brand,
                model=device.model,
                memory_size=device.memory_size,
                color=device.color
            )
        
        return cls.cache[device]


# context 对象,或者说具体的 mp3 对象,它具备了其内部属性和外部属性
class PersonalMP3:
    def __init__(self, device:Device, settings="default setting", music_list=None):
        self.mp3 = MP3Factory.get_mp3(device)
        # 省略
		...

这段代码中我们在 MP3Factory 中维护了一个字典,用内部属性(Device 具名元组 )作为键索引其对应的 flyweight 实例。

  1. 若不存在则新建后放进字典里并返回
  2. 若存在则直接返回

完整代码

现在我们将所有代码合并在一起:

from collections import namedtuple
import tracemalloc

Device = namedtuple("Device", ["brand", "model", "memory_size", "color"])

def parse_comma_data(raw_data:str, char=","):
    lines = raw_data.split("\n")
    return (l.split(char) for l in lines)

# 此时 MP3 是一个 flyweight 对象
class MP3:
    def __init__(self, brand, model, memory_size, color):
        self.brand = brand
        self.model = model
        self.memory_size = memory_size
        self.color = color
        
        # 比较占内存空间的变量
        self.equipment_parameters = bytes(1000000)
        self.system = bytes(1000000)

        
    def __str__(self):
        return f"{self.model} {self.color} {self.memory_size} with id:{id(self)}"

    def __repr__(self):
        return self.__str__()
    


# flyweight factory 用于提供 flyweight obj
class MP3Factory:
    cache = {}
    
    @classmethod
    def get_mp3(cls, device: Device):
        if device not in cls.cache:
            cls.cache[device] = MP3(
                brand=device.brand,
                model=device.model,
                memory_size=device.memory_size,
                color=device.color
            )
        
        return cls.cache[device]


# context 对象,或者说具体的 mp3 对象,它具备了其内部属性和外部属性
class PersonalMP3:
    def __init__(self, device:Device, settings="default setting", music_list=None):
        self.mp3 = MP3Factory.get_mp3(device)
        self.settings = settings
        self.music_list = music_list
        
    def add_music(self, new_music:str):
        if self.music_list is None:
            self.music_list = []
            
        self.music_list.append(new_music)
        


mp3_list = """john,sony,S1001,4G,red
cindy,sony,S1001,4G,red
simon,sony,S1001,4G,red
lucy,sony,S1001,4G,red
babala,sony,S1002,4G,red
tom,sony,S1002,4G,red
dikaer,sony,S1005,16G,red"""


def main():
    mp3_devices = {}
    for data in parse_comma_data(mp3_list):
        name, *device = data
        mp3_devices[name] = PersonalMP3(Device(*device), settings=name)
    
    return mp3_devices
    
tracemalloc.start()
mp3_list = main()
print("内存占用:", tracemalloc.get_traced_memory())
tracemalloc.stop()

for name, mp3 in mp3_list.items():
    print(name, mp3.mp3)

控制台输出:

内存占用: 6004821
john S1001 red 4G with id:1658046368208
cindy S1001 red 4G with id:1658046368208
simon S1001 red 4G with id:1658046368208
lucy S1001 red 4G with id:1658046368208
babala S1002 red 4G with id:1658046370512
tom S1002 red 4G with id:1658046370512
dikaer S1005 red 16G with id:1658046378256

注:这里打印出来的 id 不是 PersonalMP3 的 id, 而是 PersonalMP3.mp3 的 id

相比上一个版本的实现,内存的开销缩小了 23 倍(140004589/6004821),尽管每个 PersonalMP3id 还是独立的,但是每个实例下面的 mp3 属性指向的实例对象的 id 则存在相同的现象,即有些是共用的,比如上面的 john、cindy、simon、lucy

读到这里,你或许会问组件定义中的 client 怎么没提及,这段实际上是代码中的 main 函数。

未讨论问题

  1. 上面的 add_music 这里对象本身的行为是否一定要从 flyweight 对象中拆分出来?
  2. 这个模式引入什么弊病?

先吃饭,下次再补全

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值