python中的享元模式(flyweight)

这篇文章主要翻译自Design Pattern and Python Flyweight Pattern,稍微加上我自己的一点理解。

享元模式


《设计模式》中的享元是一种共享对象,能够在不同的上下文环境中同时使用,并且独立于上下文。因此,只有(隐式地)共享状态保存在享元对象本身中;而(显式地)上下文独立的状态被分别保存,在需要时传递给享元对象。本文中只讨论享元对象的创建机制以及对隐式共享状态的管理。享元模式常用在需要降低内存使用量的场景中。这种应用场景需要大量同一个对象的实例,并且这些实例拥有同样的隐式状态,而显式状态可以通过计算拿到或者存储代价比较低。

1. 严格的《设计模式》中的享元模式

严格的《设计模式》中的享元模式没有使用python中许多漂亮的特性。下面这个实现展示在一个静态地、强类型地语言中如何实现享元模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Spam(object):
def __init__(self, a, b):
self.a, self.b = a, b
class SpamFactory(self):
def __init__(self):
self.__instances = {}
def get_instance(self, a, b):
key = (a, b)
if key not in self.__instances:
self.__instance[key] = Spam(a, b)
return self.__instance[key]
class Egg(self):
def __init__(self, x, y):
self.x, self.y = x, y
class EggFactory(object):
def __init__(self):
self.__instances = {}
def get_instance(self, x, y):
key = (x, y)
if key not in self.__instances:
self.__instances[key] = Egg(x, y)
return self.__instances[key]
#----------------------------
spam_factory = SpamFactory()
egg_factory = EggFactory()
assert spam_factory.get_instance(1, 2) is spam_factory.get_instance(1, 2)
assert egg_factory.get_instance('a', 'b') is egg_factory.get_instance('a', 'b')
assert spam_factory.get_instance(1, 2) is not egg_factory.get_instance(1, 2)

但是python是一种动态类型语言,类也能作为第一类对象。通过使用*args替代固定的初始化参数,并将享元类也传递给工厂类作为初始化参数,工厂类能够更加通用化。下面就是一个这样的例子。

2. 《设计模式》中的享元模式

这个例子稍微pythonic一点,使用了*args并且将类作为参数进行传递。因此不需要为每个工厂实现单独的类。简单来说实现了一个通用工厂,这个工厂需要传递进享元类本身,产生享元类对应的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class FlyweightFactory(object):
def __init__(self, cls):
self._cls = cls
self.__instances = {}
def get_instance(self, *args, **kwargs):
key = (args, tuple(kwargs.items()))
return self.__instances.setdefault(key, self._cls(*args, **kwargs))
#--------------------------------------
class Spam(object):
def __init__(self, a, b):
self.a, self.b = a, b
class Egg(object):
def __init__(self, x, y):
self.x, self.y = x, y
class SubSpam(Spam):
pass
SpamFactory = FlyweightFactory(Spam)
EggFactory = FlyweightFactory(Egg)
SubSpamFactory = FlyweightFactory(SubSpam)
assert SpamFactory.get_instance(1, 2) is SpamFactory.get_instance(1, 2)
assert EggFactory.get_instance('a', 'b') is EggFactory.get_instance('a', 'b')
assert SubSpamFactory.get_instance(1, 2) is not SpamFactory.get_instance(1, 2)

通过python的语法,使工厂类更加通用化。需要注意SubSpamFactorySpamFactory是两个不同的类工厂,产生的实例是不同的(即使传递相同的参数,SubSpamFactory产生的实例是SubSpam的实例,是Spam的子实例)。

3. 类装饰器版本

通过使用__call__魔术方法,可以不用显式地调用get_instance方法。直接通过SpamFactory(1,2)就能得到实例。
通过SpamFactory(1,2)得调用方式得到实例与通过Spam(1,2)得方式在形式上是一样的。例如,可以通过Spam=SpamFactory(Spam)的方式实现。相当于将SpamFactory又取了个Spam的名字,或者说将名字Spam重新绑定到SpamFactory类上。这实际上就是装饰器的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Flyweight(object):
def __init__(self, cls):
self._cls = cls
self.__instances = {}
def __call__(self, *args, **kwargs):
key = (args, tuple(kwargs.items()))
return self.__instances.setdefault(key, self._cls(*args, **kwargs))
@Flyweight
class Spam(object):
def __init__(self, a, b):
self.a, self.b = a, b
@Flyweight
class Egg(object):
def __init__(self, x, y):
self.x, self.y = x, y
assert Spam(1, 2) is Spam(1, 2)
assert Egg('a', 'b') is Egg('a', 'b')
@Flyweight
class SubSpam(Spam._cls): # 不能使用Spam
pass
assert SubSpam(1, 2) is SubSpam(1, 2)
assert SubSpam(1, 2) is not Spam(1, 2)

python的装饰器是高阶函数,以可调用对象为参数产生另外一个可调用对象。@Flyweight等同于上一个例子中的Spam = SpamFactory(Spam)。注意子类SumSpam的实现。

4. 函数装饰器版本

通过对类Flyweight实现__call__方法把该类伪装成了一个函数。直接使用函数实现装饰器更加方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
def flyweight(cls):
instances = {}
return lambda *args, **kwargs: instances.setdefault(
(args, tuple(kwarges.items())),
cls(*args, **kwargs))
# 或者不使用lambda
def flyweight(cls):
instances = {}
def _wrap(*args, **kwargs):
key = (args, tuple(kwargs.items()))
return instances.setdefault(key, cls(*args, **kwargs))
return _wrap

5. 《设计模式》中的MixIn

目前为止,我们有一个代理方法(工厂函数),用来创建一个函数或者类。而这个函数或者类封装了享元类,缓存实例并且代理享元类进行实例化。工厂函数可以通过实现类方法的方式进行实例化的代理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class FlyweightMixin(object):
_instances = {}
@classmeothd
def get_instance(cls, *args, **kwargs):
return cls._instance.setdefault(
(args, tuple(kwargs.items())),
cls(*args, **kwargs))
class Spam(FLyweightMixin):
def __init__(self, a, b):
self.a, self.b = a, b
class Egg(FLyweightMixin):
def __init__(self, x, y):
self.x, self.y = x, y
assert Spam.get_instance(1, 2) is Spam.get_instance(1, 2)
assert Egg.get_instance('a', 'b') is Egg.get_instance('a', 'b')
class SubSpam(Spam):
pass
assert SubSpam.get('x', 'y') is SubSpam.get_instance('x', 'y')
assert SubSpam.get('a', 'b') is not Spam.get_instance('a', 'b')

Mixin是一种接口类,类似Java的Interface一般是作为接口存在而自己不实例化为单独的实例。

6. Minxin版本

上一版本不太安全,没有办法阻止用户绕过Flyweight直接实例化享元类。(python的类实现是不完备的,很少有绝对的方法阻止用户去做一些事情。而且,python拥有完全的反射机制,用户几乎可以透过界面做任何事情。)
通过将get_instance方法移动到__new__方法中去,我们能(一定程度上)阻止用户直接实例化享元类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 所有实例都缓存在FlyweightMixin中
class FlyweightMixin(object):
_instances = {}
def __init__(self):
raise NotImplementedException
def __new__(cls, *args, **kwargs):
return cls._instances.setdefault(
# 需要cls作为键值的一部分
(cls, args, tuple(kwargs.items())),
# 通过super和type调用享元类进行实例化
super(type(cls), cls).__new__(cls, *args, **kwargs))
class Spam(FlyweightMixin):
def __init__(self, a, b):
self.a = a
self.b = b
class Egg(FlyweightMixin):
def __init__(self, x, y):
self.x = x
self.y = y
assert Spam(1, 2) is Spam(1, 2)
assert Egg('a', 'b') is Egg('a', 'b')
assert Spam(1, 2) is not Egg(1, 2)
# Subclassing a flyweight class
class SubSpam(Spam):
pass
assert SubSpam(1,2) is SubSpam(1,2)
assert Spam(1,2) is not SubSpam(1,2)

这个版本的改动比较大,需要理解python中的__new__魔术方法。另外,作为字典_instances的键值需要增加cls,因为这种方式将所有享元类的实例全部保存在了相同的字典中。而之前的每个享元类的实例是保存在独立的空间中的。

7. 改进的装饰器版本

除了继承,类属性可以动态的添加到享元类上。python是一种动态类型,类本身能够在运行时修改。这种方法更加灵活、限制更少、更加优雅,而且能够应用到第三方类上。(不过,更加难懂)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@classmethod
def get_instance(cls, *args, **kwargs):
return cls._instances.setdefault(
(cls, args, tuple(kwargs.items())),
super(type(cls), cls).__new__(*args, **kwargs)
def flyweight(decoree):
decoree._instances = {}
# 将get_instance 修改为 享元类进行实例化时调用的函数__new__
# python中 __new__是真正的构建函数,决定实例的模板和内存结构
# 而 __init__更像初始化函数,用来给实例变量赋予初值
decoree.__new__ = get_instance
return decoree
#---------------------------
@flyweight
class Spam(object):
def __init__(self, a, b):
self.a = a
self.b = b
@flyweight
class Egg(object):
def __init__(self, x, y):
self.x = x
self.y = y
assert Spam(1, 2) is Spam(1, 2)
assert Egg('a', 'b') is Egg('a', 'b')
assert Spam(1, 2) is not Egg(1, 2)
# Subclassing a flyweight class
class SubSpam(Spam):
pass
assert SubSpam(1,2) is SubSpam(1,2)
assert Spam(1,2) is not SubSpam(1,2)

8. 超类版本

上一个版本实际上起到了超类的作用,通过修改享元类的构建函数__new__决定创建新的实例,还是使用已经缓存的实例。直接通过超类的语法当然也能实现。(这种方式非常复杂,而且不易阅读和维护,不建议这样使用超类,甚至不建议使用超类语法本身)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class MetaFlyweight(type):
def __new__(cls, *args, **kwargs):
# 初始化类本身,类作为超类的实例进行初始化,可以忽略
type.__init__(cls, *args, **kwargs)
cls._instances = {}
# 修改类的构建函数
cls.__new__ = cls._get_instance
def _get_instance(cls, *args, **kwargs):
return cls._instances.setdefault(
(args, tuple(kwargs.items())),
# 调用 享元类 本身去实例化
super(cls, cls).__new__(*args, **kwargs))
class Spam(object):
__metaclass__ = MetaFlyweight
def __init__(self, a, b):
self.a = a
self.b = b
class Egg(object):
__metaclass__ = MetaFlyweight
def __init__(self, x, y):
self.x = x
self.y = y
assert Spam(1, 2) is Spam(1, 2)
assert Egg('a', 'b') is Egg('a', 'b')
assert Spam(1, 2) is not Egg(1, 2)
# Subclassing a flyweight class
class SubSpam(Spam):
pass
assert SubSpam(1,2) is SubSpam(1,2)
assert Spam(1,2) is not SubSpam(1,2)

9. 函数式超类

对于python,所有的方法、属性都可以动态的添加。可以通过函数式的方式进行操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@classmethod
def _get_instance(cls, *args, **kwargs):
return cls.__instances.setdefault(
(args, tuple(kwargs.items())),
super(type(cls), cls).__new__(*args, **kwargs))
def metaflyweight(name, parents, attrs):
# 通过实例化type,动态构建享元类本身
cls = type(name, parents, attrs)
# 设置类属性
cls.__instances = {}
# 修改享元类实例化时的逻辑
cls.__new__ = _get_instance
return cls
#----------------------------------------------------------
class Spam(object):
__metaclass__ = metaflyweight
def __init__(self, a, b):
self.a = a
self.b = b
class Egg(object):
__metaclass__ = metaflyweight
def __init__(self, x, y):
self.x = x
self.y = y
assert Spam(1, 2) is Spam(1, 2)
assert Egg('a', 'b') is Egg('a', 'b')
assert Spam(1, 2) is not Egg(1, 2)
# Subclassing a flyweight class
class SubSpam(Spam):
pass
assert SubSpam(1,2) is SubSpam(1,2)
assert Spam(1,2) is not SubSpam(1,2)

10. Pure函数超类版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
metaflyweight = lambda name, parents, attrs: type(
name,
parents,
# 直接通过__new__, __instances加入attrs
dict(attrs.items() + [
('__instances', {}),
('__new__', classmethod(
lambda cls, *args, **kwargs: cls.__instances.setdefault(
(args, tuple(kwargs.items())),
super(type(cls), cls).__new__(*args, **kwargs))
)
)
])
)
#----------------------------------------------------------
class Spam(object):
__metaclass__ = metaflyweight
def __init__(self, a, b):
self.a = a
self.b = b
class Egg(object):
__metaclass__ = metaflyweight
def __init__(self, x, y):
self.x = x
self.y = y
assert Spam(1, 2) is Spam(1, 2)
assert Egg('a', 'b') is Egg('a', 'b')
assert Spam(1, 2) is not Egg(1, 2)
# Subclassing a flyweight class
class SubSpam(Spam):
pass
assert SubSpam(1,2) is SubSpam(1,2)
assert Spam(1,2) is not SubSpam(1,2)


讨论


考虑到继承和反射。

如果需要继承或者反射,装饰器的方式是无效的(装饰器版本、函数式装饰器版本)。实际上,在这些版本中没有办法直接访问最原始定义的那个享元类。一些类属性(type, docstring, name, super classes等)不再能够访问。基于同样原因,我们不能使用装饰器作为一个超类(即不能通过@derector的方式实现超类的功能)。

如果需要使用反射,一些超类的属性能够复制给被装饰对象。例如,通过手动或者functools模块中的@wraps/@update_wrapper。但是不是所有的属性都能够复制给被装饰对象。

如果需要使用继承,对于装饰器版本来说,需要通过_cls访问真正的享元类本身。对于函数式装饰器版本来说就完全没有办法了。

如果需要扩展享元类(添加属性、继承等),改善的装饰器版本更加适合。Mixin的版本也是可以直接使用的,因为处理了子类继承的问题。当然,超类的版本也运行的非常好,因为超类原本就是可以继承的。

考虑到垃圾回收

如果要应用在生产环境,这些实例需要考虑垃圾回收。实际上,因为享元工厂中的字典缓存了实例,实例会一直被引用。这些实例在整个程序生命周期都不能通过自动垃圾回收而收回,导致大量的内存使用量以及一些错误的行为。

如果希望享元模式能够如期望般运行,可以使用python的weakref模块中的弱引用(weak reference)。弱引用不会阻止垃圾回收机制的运行,当实例不再有引用时可以被正常的回收。另外weakref中有个对象叫做WeakValueDictionary,行为与dict一致不过其值自然是弱引用的。

考虑到可用性

我们考虑两种截然不同的实现方式:

  1. 代理享元类:享元类被封装到对象内,通过代理实现想要的功能。例如《设计模式》的工厂版本、装饰器版本。
  2. 修改享元类:将想要的功能直接添加入享元类中。例如改善的装饰器版本,超类版本,Mixin版本。

从最终使用的角度考虑,所有的实现方式都是等价且透明的,只需要在超类、装饰器、mixin中3选1即可。

从元数据和继承的角度考虑,修改的方式比代理的方式更加合适。修改的方式也没有额外的开销,而代理的方式需要为每一个享元类额外创建一个(代理享元类的)对象。

然而,代理的方式比较普遍适用,而修改的方式只能适用于类。(不过鉴于python中一切皆对象的概念,修改的方式对函数也是可以的)

最后,代理和装饰器比超类和继承更加具有柔性,后者很难应用于已经实现了的类中。改进的装饰器版本看起来更加有意思(估计那个super(type(cls), cls)就够让人奇怪的了)。