享元模式是一种内存优化模式。一般情况我们很少会关注内存优化问题,内置的垃圾收集器会处理他们。但是在开发拥有许多关联对象的大型应用程序时,关注内存是否充裕可以带来巨大的收益。
在现实生活中,经常是程序仅仅在暴露出内存问题后才想起实现享元模式。在某些情况下,从最初就设计优化是很有意义的。但是,过早的优化经常会让程序极其复杂以至于难以维护。
享元模式背后的基本思想是,保证共享统一状态的对象可以同时使用该共享状态的内存。想象一下汽车销售的库存系统。每辆车都有一个特定的序列和一种特定的眼色。但是同一型号所有汽车的大部分细节都是相同的。例如,本田飞度的DX车型是一种具备少量功能的最基本类型。LX车型多出了A/C、倾斜传感器、巡航系统、电动车窗和门锁。而运动款则带有花哨的轮廓、USB充电器和一个扰流板。如果没有享元模式,每个单独的汽车对象都要储存一个长长的清单,包含它有什么功能,没有什么功能。考虑到本田汽车的年销量,全部统计会产生巨大的内存浪费。而使用享元模式,我们可以使用共享对象储存于型号相关的特性列表,每辆车在拥有自己的序列号和颜色的同时只需要简单的对该型号进行引用。
享元模式的UML结构图如下:每个享元都没有指定状态,在任何需要对特定状态执行操作的时候,该状态就需要被代码调用,传递到该享元处。一般来说,享元返回的工厂是一个单独的对象,他是为一个能够识别享元的给定关键词而返回的。他的工作原理就像单例模式一样——如果享元存在,则返回他;否则,重新创建一个。在许多语言中,工厂不是一个单独的对象,而是作为Flyweight类本身的静态方法来执行。
这两种方式都行得通,但在python中,享元工厂经常使用新的__new__
构造函数来实现,类似于单例模式的实现。不同于单例模式的是,单例模式只需要返回类的一个实例,而享元则需要我们能够依赖于关键词返回不同的实例。我们可以将项目储存在字典中,并根据键值来查找他们。然而这种解决方案是有问题的,因为只要项目在字典中存在,它将一直会保留在内存中。当然,当我们卖完了LX车型,那么享元就不在被需要了,但它仍然会保留在字典中。当然,我们也可以在卖出一辆车后对他们进行清理,但这是垃圾收集器应该做的事。
Python的weakref
模块可以帮助我们解决这个问题。该模块提供了一个WeakValueDictionary
对象,这基本可以让我们将项目储存在一个字典中,而不需要垃圾收集器来处理他们。如果某个值存在于一个弱引用的字典中,而且还没有任何其他对它的引用(就像我们卖完的LX车型),垃圾收集器将最终为我们把它处理掉。
让我们首先为这些汽车享元建立工厂:
import weakref
class CarModel(object):
_models = weakref.WeakValueDictionary()
def __new__(cls, model_name, *args, **kwargs):
model = cls._models.get(model_name)
if not model:
model = super().__new__(cls)
cls._models[model_name] = model
return model
基本上,每当我们通过给定的名字构造一个新的享元时,我们首先在弱引用的字典中查找看看;如果他存在,我们就返回这个车型,如果没有,我们就创建一个新车型。无论采用哪种方式,享元的__init__
函数都将被调用一次,不管他是一个新的还是已有的对象。因此,我们的__init__
可能看起来是这样的:
def __init__(self, model_name, air=False, tilt=False,
cruise_control=False, power_locks=False,
alloy_wheel=False, usb_charger=False):
if not hasattr(self, 'initted'):
self.model_name = model_name
self.air = air
self.tilt = tilt
self.cruise_control = cruise_control
self.power_locks = power_locks
self.alloy_wheel = alloy_wheel
self.usb_charger = usb_charger
self.initted = True
if 语句确保当__init__
函数第一次被调用时,我们才会初始化对象。这意味着我们在稍后通过车型名称调用工厂方法就可以获得相同的享元对象。然而,如果外部没有对他的引用存在,享元对象将被作为垃圾回收,所以需要小心,不要意外的创建一个新的包含空值的享元。
让我们再为享元增加一个方法,假装查看某一特定车型的序列号,以确认此车是否出过任何事故。这个方法需要对不同车辆的不同序列号进行访问;序列号不能以享元的形式储存。因此,这个数据必须通过代码调用传递到这个方法中:
def check_serial(self, serial_number):
print('Sorry, we are unable to check'
'the serial number {0} on the {1}'
'at this time'.format(
serial_number,self.model_name))
我们可以定义一个类来存储附加信息,以及对享元的引用:
class Car(object):
def __init__(self, model, color, serial):
self.model = model
self.color = color
self.serial = serial
def check_serial(self):
return self.model.check_serial(self.serial)
我们还可以追踪记录可用的车型以及这种车型中的某辆汽车:
dx = CarModel('FIT DX')
lx = CarModel('FIT LX', air=True, cruise_control=True,
power_locks=True, tilt=True)
car1 = Car(dx, 'blue', '12345')
car2 = Car(lx, 'black', '123456')
car3 = Car(lx, 'red', '12347')
id(lx)
del(lx)
del(car3)
import gc # 垃圾回收机制,可以参考https://blog.csdn.net/yueguanghaidao/article/details/11274737
gc.collect()
lx = CarModel('FIT LX', air=True, cruise_control=True,
power_locks=True, tilt=True)
id(lx)
lx = CarModel('FIT LX') # 第二次初始化,id未变,未进行__init__初始化
id(lx)
lx.air
# 输出:
2218650658072
190
2218650658072
2218650658072
True
id
函数能够告诉我们某个对象的唯一标识符。当我们第二次调用它并删除所有对LX车型的引用,并强制进行垃圾回收之后,我们可以看到ID并未发生更改(原书中写的是未发生改变,但是我这里验证未发生改变,不知道为什么?)。当我们后面的第二次构建CarModel时,即便没有提供任何参数,变量air仍然被设置为True。这意味着该对象第二次没有被初始化,就像我们设计的那样。
显而易见,采用享元模式可能比单纯的将特性储存在某个汽车类中更加复杂。我们应该在拥有大量相似的对象的时候使用,将他们的相同特性整合进一个享元中可以极大的减少对内存的消耗。享元模式就是专为节省内存而设计的。
参考:
《Python3 面向对象编程》
网友评论