享元模式是一种用于解决资源和性能压力时会使用到的设计模式,它的核心思想是通过引入数据共享来提升性能

我们知道程序开发的重点是对现实世界的抽象,那么相似的对象必然有某些相同的属性或行为。比如游戏中,每个角色的均可以做一些相同的动作,同样类型的角色有更多相同的动作。那么,出于优化性能减小资源开销的目的,在应用需要创建大量的计算代价大但共享许多属性的对象时,可以使用享元。重点在于将不可变(可共享)的属性与可变的属性区分开。

下面用书中的实际的代码示例来说明。

假设场景,我们需要模拟一片树林,其中有不同年龄的苹果树、梨树和樱桃树分布在不同的位置。我们先定义Tree这个类,并设置TreeType为几种树木的枚举。

from enum import Enum


TreeType = Enum('TreeType', 'apple_tree cherry_tree peach_tree')

class Tree(object):
    def __init__(self, tree_type, age, x, y):
        self.tree_type = tree_type
        self.age = age
        self.x = x
        self.y = y

    def render(self):
        print('类型 {} 年龄 {} 位置 ({}, {})'.format(self.tree_type, self.age, self.x, self.y))

如此我们就成功创建了一个Tree的类,其中的tree_type用于标识每个实例到底是哪一种树。

但是,这样一来,我们就会发现,当树林中树木非常多的时候,我们的对象的数量将会急剧膨胀,在数量很大或者资源很紧张的时候是不可接受的。所以我们需要考虑使用享元模式来提取通用数据以节约资源。

这里我们要明确两个知识点,第一是Python中类属性与实例属性的区别,第二是def __new__方法与def __init__方法的区别。具体知识点不是本文讨论重点,请各位自行查阅资料学习,这里直接给出使用这两个知识点实现享元模式以解决之前方案问题的办法。

class Tree(object):
    pool = dict()

    def render(self, age, x, y):
        print('类型 {} 年龄 {} 位置 ({}, {})'.format(self.tree_type, age, x, y))

    def __new__(cls, tree_type):
        t = cls.pool.get(tree_type, None)
        if t:
            pass
        else:
            t = object.__new__(cls)
            cls.pool[tree_type] = t
            t.tree_type = tree_type
        return t

Tree类如此修改一番,我们就实现了享元模式。简单做一个讲解。

我们为Tree类型提供了一个类属性pool代表这个类的对象池——可以理解为一个对象数据的缓存,而def __new__方法,将Tree类改造为一个元类,实现了引用自身的目的。于是,在每次创建新的对象时先到pool中检查是否有该类型的的对象存在,如果存在就直接返回之前创建好的那个对象,如果不存在则在pool中添加这个新的对象,如此一来就实现了对象的复用。这里要注意的是,我们使用了__new__方法后,移除了__init__方法,并把agexy等可变数据都放到了其他地方,就是为了保留最通用的数据,这也是实现享元模式的核心

下面我们测试一下:

def main():
    import random
    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('生成并渲染的树木: {}棵'.format(tree_counter))
    print('创建的树木对象: {}个'.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)))
    t4.render(4, 4, 4)
    t5.render(5, 5, 5)
    t6.render(6, 6, 6)

结果输出如下:

$ python flyweight.py
类型 TreeType.apple_tree 年龄 6 位置 (98, 11)
类型 TreeType.apple_tree 年龄 20 位置 (90, 43)
类型 TreeType.apple_tree 年龄 10 位置 (100, 7)
类型 TreeType.apple_tree 年龄 6 位置 (65, 40)
类型 TreeType.apple_tree 年龄 11 位置 (56, 99)
类型 TreeType.apple_tree 年龄 21 位置 (15, 33)
类型 TreeType.apple_tree 年龄 26 位置 (9, 9)
类型 TreeType.apple_tree 年龄 6 位置 (26, 94)
类型 TreeType.apple_tree 年龄 26 位置 (89, 96)
类型 TreeType.apple_tree 年龄 13 位置 (50, 26)
类型 TreeType.cherry_tree 年龄 28 位置 (17, 37)
类型 TreeType.cherry_tree 年龄 6 位置 (27, 47)
类型 TreeType.cherry_tree 年龄 29 位置 (31, 15)
类型 TreeType.peach_tree 年龄 23 位置 (63, 99)
类型 TreeType.peach_tree 年龄 5 位置 (9, 76)
类型 TreeType.peach_tree 年龄 30 位置 (58, 48)
类型 TreeType.peach_tree 年龄 14 位置 (60, 35)
类型 TreeType.peach_tree 年龄 3 位置 (64, 17)
生成并渲染的树木: 18棵
创建的树木对象: 3个
522952945848 == 522952945848? True
522952945848 == 522954153824? False
类型 TreeType.cherry_tree 年龄 4 位置 (4, 4)
类型 TreeType.cherry_tree 年龄 5 位置 (5, 5)
类型 TreeType.apple_tree 年龄 6 位置 (6, 6)

可以看到,最后的内存单元编号证明,同样类型的树木对象共享了同一个树木类型数据。但会改变的部分数据——年龄,位置——又互不影响,在树木数量超大时,将有效提升性能。


最后要注意:享元模式不能依赖对象的id。在上面的测试实例中可以看到,因为使用了享元模式,所以本来是不同的两棵树,在做id对比时,却是同一个对象,这是因为当前一个对象动作完成后,后一个对象就覆盖了前一个对象。如果不注意,可能会在开发中埋下很大的隐患。要测试也很简单,我们将Tree类的代码改成下面的形式:

from enum import Enum


TreeType = Enum('TreeType', 'apple_tree cherry_tree peach_tree')

class Tree(object):
    tree_type = ''
    pool = dict()

    def __init__(self, tree_type, age):
        self.age = age

    def render(self, x, y):
        print('类型 {} 年龄 {} 位置 ({}, {})'.format(self.tree_type, self.age, x, y))

    def __new__(cls, tree_type, age):
        t = cls.pool.get(tree_type, None)
        if t:
            pass
        else:
            t = object.__new__(cls)
            cls.pool[tree_type] = t
            t.tree_type = tree_type
        return t


def main():
    import random
    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,
                  rnd.randint(age_min, age_max))
        t1.render(rnd.randint(min_point, max_point),
            rnd.randint(min_point, max_point))
        tree_counter += 1

    for _ in range(3):
        t2 = Tree(TreeType.cherry_tree,
                  rnd.randint(age_min, age_max))
        t2.render(rnd.randint(min_point, max_point),
            rnd.randint(min_point, max_point))
        tree_counter += 1

    for _ in range(5):
        t3 = Tree(TreeType.peach_tree,
                  rnd.randint(age_min, age_max))
        t3.render(rnd.randint(min_point, max_point),
            rnd.randint(min_point, max_point))
        tree_counter += 1

    print('生成并渲染的树木: {}棵'.format(tree_counter))
    print('创建的树木对象: {}个'.format(len(Tree.pool)))

    t4 = Tree(TreeType.cherry_tree,
              4)
    t5 = Tree(TreeType.cherry_tree,
              5)
    t6 = Tree(TreeType.apple_tree,
              6)
    print('{} == {}? {}'.format(id(t4), id(t5), id(t4) == id(t5)))
    print('{} == {}? {}'.format(id(t5), id(t6), id(t5) == id(t6)))
    t4.render(4, 4)
    t5.render(5, 5)
    t6.render(6, 6)

if __name__ == '__main__':
    main()

这里将age放到了__init__方法中,按照设想,他应该成为对象自身的属性,即每个对象均不同,那么我们跑一跑代码:

$ python flyweight.py
类型 TreeType.apple_tree 年龄 19 位置 (57, 8)
类型 TreeType.apple_tree 年龄 26 位置 (21, 16)
类型 TreeType.apple_tree 年龄 30 位置 (33, 72)
类型 TreeType.apple_tree 年龄 18 位置 (98, 79)
类型 TreeType.apple_tree 年龄 9 位置 (90, 15)
类型 TreeType.apple_tree 年龄 25 位置 (17, 13)
类型 TreeType.apple_tree 年龄 15 位置 (100, 86)
类型 TreeType.apple_tree 年龄 8 位置 (45, 4)
类型 TreeType.apple_tree 年龄 19 位置 (11, 6)
类型 TreeType.apple_tree 年龄 18 位置 (18, 46)
类型 TreeType.cherry_tree 年龄 25 位置 (42, 68)
类型 TreeType.cherry_tree 年龄 9 位置 (8, 50)
类型 TreeType.cherry_tree 年龄 1 位置 (60, 28)
类型 TreeType.peach_tree 年龄 2 位置 (42, 73)
类型 TreeType.peach_tree 年龄 7 位置 (87, 59)
类型 TreeType.peach_tree 年龄 19 位置 (26, 23)
类型 TreeType.peach_tree 年龄 21 位置 (3, 22)
类型 TreeType.peach_tree 年龄 16 位置 (43, 73)
生成并渲染的树木: 18棵
创建的树木对象: 3个
332890664464 == 332890664464? True
332890664464 == 332889074432? False
类型 TreeType.cherry_tree 年龄 5 位置 (4, 4)
类型 TreeType.cherry_tree 年龄 5 位置 (5, 5)
类型 TreeType.apple_tree 年龄 6 位置 (6, 6)

最后几行,在(4, 4)位置的t4樱桃树年龄本该是4,却变成了5,即位置在(5, 5)的t5樱桃树的年龄,说明t4中的共享部分数据其实已经被t5所覆盖。