这篇文章主要翻译自Design Pattern and Python Flyweight Pattern,稍微加上我自己的一点理解。
享元模式
《设计模式》中的享元
是一种共享对象,能够在不同的上下文环境中同时使用,并且独立于上下文。因此,只有(隐式地)共享状态保存在享元对象本身中;而(显式地)上下文独立的状态被分别保存,在需要时传递给享元对象。本文中只讨论享元对象的创建机制以及对隐式共享状态的管理。享元模式常用在需要降低内存使用量的场景中。这种应用场景需要大量同一个对象的实例,并且这些实例拥有同样的隐式状态,而显式状态可以通过计算拿到或者存储代价比较低。
1. 严格的《设计模式》中的享元模式
严格的《设计模式》中的享元模式没有使用python中许多漂亮的特性。下面这个实现展示在一个静态地、强类型地语言中如何实现享元模式。
|
|
但是python是一种动态类型语言,类也能作为第一类对象
。通过使用*args
替代固定的初始化参数,并将享元类也传递给工厂类作为初始化参数,工厂类能够更加通用化。下面就是一个这样的例子。
2. 《设计模式》中的享元模式
这个例子稍微pythonic一点,使用了*args
并且将类作为参数进行传递。因此不需要为每个工厂实现单独的类。简单来说实现了一个通用工厂,这个工厂需要传递进享元类本身,产生享元类对应的实例。
|
|
通过python的语法,使工厂类更加通用化。需要注意SubSpamFactory
与SpamFactory
是两个不同的类工厂,产生的实例是不同的(即使传递相同的参数,SubSpamFactory产生的实例是SubSpam的实例,是Spam的子实例)。
3. 类装饰器版本
通过使用__call__
魔术方法,可以不用显式地调用get_instance
方法。直接通过SpamFactory(1,2)
就能得到实例。
通过SpamFactory(1,2)
得调用方式得到实例与通过Spam(1,2)
得方式在形式上是一样的。例如,可以通过Spam=SpamFactory(Spam)
的方式实现。相当于将SpamFactory
又取了个Spam
的名字,或者说将名字Spam
重新绑定到SpamFactory
类上。这实际上就是装饰器
的功能。
|
|
python的装饰器是高阶函数,以可调用对象为参数产生另外一个可调用对象。@Flyweight
等同于上一个例子中的Spam = SpamFactory(Spam)
。注意子类SumSpam
的实现。
4. 函数装饰器版本
通过对类Flyweight
实现__call__
方法把该类伪装成了一个函数。直接使用函数实现装饰器更加方便。
|
|
5. 《设计模式》中的MixIn
目前为止,我们有一个代理方法(工厂函数),用来创建一个函数或者类。而这个函数或者类封装了享元类,缓存实例并且代理享元类进行实例化。工厂函数可以通过实现类方法的方式进行实例化的代理。
|
|
Mixin是一种接口类,类似Java的Interface一般是作为接口存在而自己不实例化为单独的实例。
6. Minxin版本
上一版本不太安全
,没有办法阻止用户绕过Flyweight直接实例化享元类。(python的类实现是不完备的,很少有绝对的方法阻止用户去做一些事情。而且,python拥有完全的反射机制,用户几乎可以透过界面做任何事情。)
通过将get_instance
方法移动到__new__
方法中去,我们能(一定程度上)阻止用户直接实例化享元类。
|
|
这个版本的改动比较大,需要理解python中的__new__
魔术方法。另外,作为字典_instances
的键值需要增加cls
,因为这种方式将所有享元类的实例全部保存在了相同的字典中。而之前的每个享元类的实例是保存在独立的空间中的。
7. 改进的装饰器版本
除了继承,类属性可以动态的添加到享元类上。python是一种动态类型,类本身能够在运行时修改。这种方法更加灵活、限制更少、更加优雅,而且能够应用到第三方类上。(不过,更加难懂)
|
|
8. 超类版本
上一个版本实际上起到了超类的作用,通过修改享元类的构建函数__new__
决定创建新的实例,还是使用已经缓存的实例。直接通过超类的语法当然也能实现。(这种方式非常复杂,而且不易阅读和维护,不建议这样使用超类,甚至不建议使用超类语法本身)
|
|
9. 函数式超类
对于python,所有的方法、属性都可以动态的添加。可以通过函数式的方式进行操作。
|
|
10. Pure函数超类版本
|
|
讨论
考虑到继承和反射。
如果需要继承或者反射,装饰器的方式是无效的(装饰器版本、函数式装饰器版本)。实际上,在这些版本中没有办法直接访问最原始定义的那个享元类。一些类属性(type, docstring, name, super classes等)不再能够访问。基于同样原因,我们不能使用装饰器作为一个超类(即不能通过@derector的方式实现超类的功能)。
如果需要使用反射,一些超类的属性能够复制给被装饰对象。例如,通过手动或者functools模块中的@wraps/@update_wrapper
。但是不是所有的属性都能够复制给被装饰对象。
如果需要使用继承,对于装饰器版本来说,需要通过_cls
访问真正的享元类本身。对于函数式装饰器版本来说就完全没有办法了。
如果需要扩展享元类(添加属性、继承等),改善的装饰器版本更加适合。Mixin的版本也是可以直接使用的,因为处理了子类继承的问题。当然,超类的版本也运行的非常好,因为超类原本就是可以继承的。
考虑到垃圾回收
如果要应用在生产环境,这些实例需要考虑垃圾回收。实际上,因为享元工厂中的字典缓存了实例,实例会一直被引用。这些实例在整个程序生命周期都不能通过自动垃圾回收而收回,导致大量的内存使用量以及一些错误的行为。
如果希望享元模式能够如期望般运行,可以使用python的weakref模块中的弱引用
(weak reference)。弱引用不会阻止垃圾回收机制的运行,当实例不再有引用时可以被正常的回收。另外weakref中有个对象叫做WeakValueDictionary
,行为与dict
一致不过其值自然是弱引用的。
考虑到可用性
我们考虑两种截然不同的实现方式:
- 代理享元类:享元类被封装到对象内,通过代理实现想要的功能。例如《设计模式》的工厂版本、装饰器版本。
- 修改享元类:将想要的功能直接添加入享元类中。例如改善的装饰器版本,超类版本,Mixin版本。
从最终使用的角度考虑,所有的实现方式都是等价且透明的,只需要在超类、装饰器、mixin中3选1即可。
从元数据和继承的角度考虑,修改的方式比代理的方式更加合适。修改的方式也没有额外的开销,而代理的方式需要为每一个享元类额外创建一个(代理享元类的)对象。
然而,代理的方式比较普遍适用,而修改的方式只能适用于类。(不过鉴于python中一切皆对象的概念,修改的方式对函数也是可以的)
最后,代理和装饰器比超类和继承更加具有柔性,后者很难应用于已经实现了的类中。改进的装饰器版本看起来更加有意思(估计那个super(type(cls), cls)就够让人奇怪的了)。