python 实现 享元模式

本文的目录地址

本文的代码地址

由于对象创建的开销,面向对象的系统可能会面临性能问题。性能问题通常在资源受限的嵌入式系统中出现,比如智能手机和平板电脑。大型复杂系统中也可能会出现同样的问题,因为要在其中创建大量对象(用户),这些对象需要同时并存。

这个问题之所以会发生,是因为当我们创建一个新对象时,需要分配额外的内存。虽然虚拟内存理论上为我们提供了无限制的内存空间,但现实却并非如此。如果一个系统耗尽了所有的物理内存,就会开始将内存页替换到二级存储设备,通常是硬盘驱动器(Hard Disk Drive,HDD)。在多数情况下,由于内存和硬盘之间的性能差异,这是不能接受的。固态硬盘(Solid State Drive,SSD)的性能一般比硬盘更好,但并非人人都使用SSD,SSD并不会很快全面替代硬盘。

除内存使用之外,计算性能也是一个考虑点。图形软件,包括计算机游戏,应该能够极快地渲染3D信息(例如,有成千上万颗树的森林或满是士兵的村庄)。如果一个3D地带的每个对象都是单独创建,未使用数据共享,那么性能将是无法接受的。

作为软件工程师,我们应该编写更好的软件来解决软件问题,而不是要求客户购买更多更好的硬件。享元设计模式通过为相似对象引入数据共享来最小化内存使用,提升性能。一个享元(Flyweight)就是一个包含状态独立的不可变(又称固有的)数据的共享对象。依赖状态的可变(又称非固有的)数据不应是享元的一部分,因为每个对象的这种信息都不同,无法共享。如果享元需要非固有的数据,应该由客户端代码显示地提供。

用一个例子可能有助于解释实际场景中如何使用享元模式。假设我们正在设计一个性能关键的游戏,例如第一人称射击(First-Person Shooter,FPS)游戏。在FPS游戏中,玩家(士兵)共享一些状态,如外在表现和行为。例如,在《反恐精英》游戏中,同一团队(反恐精英或恐怖分子)的所有士兵看起来都是一样的(外在表现)。同一个游戏中,(两个团队的)所有士兵都有一些共同的动作,比如,跳起、低头等(行为)。这意味着我们可以创建一个享元来包含所有共同的数据。当然,士兵也有许多因人而异的可变数据,这些数据不是享元的一部分,比如,枪支、健康状况和地理位置等。

现实生活中的例子

享元模式(Flyweight pattern)是一个用于优化的设计模式。因此,要找一个合适的现实生活的例子不太容易。我们可以把享元看做现实生活中的缓存区。例如,许多书店都有专用的书架来摆放最新和流行的出版物。这就是一个缓存区,你可以先在这些专用书架上看看有没有正在找的书籍,如果没找到,则可以让图书管理员来帮你。

软件的例子

Exaile音乐播放器使用享元来复用通过相同URL识别的对象(在这里是指音乐歌曲)。创建一个与已有对象的URL相同的新对象是没有意义的,所以复用相同的对象来节约资源。

扫描二维码关注公众号,回复: 1911460 查看本文章

Peppy是一个用Python语言实现的类XEmacs编辑器,它使用享元模式存储major mode状态栏的状态。这是因为除非用户修改,否则所有状态栏共享相同的属性。

应用案例

享元旨在优化性能和内存使用。所有嵌入式系统(手机、平板电脑、游戏终端和微控制器等)和性能关键的应用(游戏、3D图形处理和实时系统等)都能从其获益。

若想要享元模式有效,需要满足GoF的《设计模式》一书罗列的以下几个条件。

  • 应用需要使用大量的对象
  • 对象太多,存储/渲染它们的代价太大。一旦移除对象中的可变状态(因为在需要之时,应该由客户端代码显示地传递给享元),多组不同的对象可被相对更少的共享对象所替代。
  • 对象ID对于应用不重要。对象共享会造成ID比较的失败,所以不能依赖对象ID(那些在客户端代码看来不同的对象,最终具有相同的ID)。

实现

由于之前已提到树的例子,那么就来看看如何实现它。在这个例子中,我们将构造一小片水果树的森林,小到能确保在单个终端页面中阅读整个输出。然而,无论你构造的森林有多大,内存分配都保持相同。下面这个Enum类型变量描述三种不同种类的水果树。

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

在深入代码之前,我们稍稍解释一下memoization与享元模式之间的区别。memoization是一种优化技术,使用一个缓存来避免重复计算那些在更早的执行步骤中已经计算好的结果。memoization并不是只能应用于某种特定的编程方式,比如面向对象编程(Object-Oriented Programming,OOP)。在Python中,memoization可以应用于方法和简单的函数。享元则是一种特定于面向对象编程优化的设计模式,关注的是共享对象数据。

在Python中,享元可以以多种方式实现。pool变量是一个对象池(换句话说,是我们的缓存)。注意:pool是一个类属性(类的所有实例共享的一个变量)。使用特殊方法__new__(这个方法在__init__之前被调用),我们把Tree类变换成一个元类,元类支持自引用。这意味着cls引用的是Tree类。当客户端要创建Tree的一个实例时,会以tree_type参数传递树的种类。树的种类用于检查是否创建过相同种类的树。如果是,则返回之前创建的对象;否则,将这个新的树种添加到池中,并返回相应的新对象,如下所示。

def __new__(cls, tree_type):
        obj=cls.pool.get(tree_type, None)
        if not obj:
            obj=object.__new__(cls)
            cls.pool[tree_type]=obj
            obj.tree_type=tree_type
        return obj

方法render()用于在屏幕上渲染一棵树。注意,享元不知道的所有可变(外部的)信息都需要客户端代码显示地传递。在当前案例中,每棵树都用到一个随机的年龄和一个x,y形式的位置。为了让render()更加有用,有必要确保没有树会被渲染到另一颗之上。你可以考虑把这个作为练习。如果你想让渲染更加有趣,可以使用一个图形工具包,比如Tkinter或Pygame。

 def render(self,age,x,y):
        print('render a tree of type {} and age {} at ({},{})'.format(self.tree_type,
                                                                      age,x,y))

main()函数展示了我们可以如何使用享元模式。一棵树的年龄是1-30年之间的一个随机数。坐标使用1-100之间的随机数。虽然渲染了18棵树,但仅分配了3颗树的内存。输出的最后一行证明当使用享元时,我们不能依赖对象的ID。函数id()会返回对象的内存地址。Python规范并没有要求id()返回对象的内存地址,只是要求id()为每个对象返回一个唯一性ID,不过CPython(Python的官方实现)正好使用对象的内存地址作为对象唯一性ID。在我们的例子中,即使两个对象看起来不相同,但是如果它们属于同一个享元家族(在这里,家族由tree_type定义),那么它们实际上有相同的ID。当然,不同ID的比较仍然可用于不同家族的对象,但这仅在客户端知道实现细节的情况下才可行(通常并非如此)。

def main():
    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('trees rendered: {}'.format(tree_counter))
    print('trees actually created: {}'.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(t6), id(t5), id(t6) == id(t5)))

完整的代码(文件flyweight.py

执行上面的程序的结果:

render a tree of type TreeType.apple_tree and age 28 at (32,67)
render a tree of type TreeType.apple_tree and age 12 at (41,25)
render a tree of type TreeType.apple_tree and age 20 at (54,16)
render a tree of type TreeType.apple_tree and age 29 at (88,50)
render a tree of type TreeType.apple_tree and age 24 at (42,93)
render a tree of type TreeType.apple_tree and age 20 at (38,46)
render a tree of type TreeType.apple_tree and age 9 at (36,89)
render a tree of type TreeType.apple_tree and age 30 at (66,96)
render a tree of type TreeType.apple_tree and age 18 at (87,62)
render a tree of type TreeType.apple_tree and age 12 at (52,2)
render a tree of type TreeType.cherry_tree and age 8 at (49,43)
render a tree of type TreeType.cherry_tree and age 27 at (64,79)
render a tree of type TreeType.cherry_tree and age 15 at (50,31)
render a tree of type TreeType.peach_tree and age 20 at (15,80)
render a tree of type TreeType.peach_tree and age 1 at (60,74)
render a tree of type TreeType.peach_tree and age 6 at (21,31)
render a tree of type TreeType.peach_tree and age 5 at (10,12)
render a tree of type TreeType.peach_tree and age 4 at (53,54)
trees rendered: 18
trees actually created: 3
139957559065120 == 139957559065120? True
139957566714712 == 139957559065120? False

相关的设计模式

  • Proxy模式

如果生成实例的处理需要花费较长时间,那么使用Flyweight模式可以提高程序的处理速度。

而Proxy模式则是通过设置代理提高程序的处理速度。

  • Composite模式

有时可以使用Flyweight模式共享Composite模式中的Leaf角色。





猜你喜欢

转载自blog.csdn.net/hbu_pig/article/details/80568749
今日推荐