python设计模式之享元模式

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

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

除内存使用之外,计算性能也是一个考虑点。图形软件,包括计算机游戏,应该能够极快地

渲染3D信息(例如,有成千上万棵树的森林或满是士兵的村庄)。如果一个3D地带的每个对象都

是单独创建,未使用数据共享,那么性能将是无法接受的。

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

用一个例子可能有助于解释实际应用场景中如何使用享元模式。假设我们正在设计一个性能

关键的游戏,例如第一人称射击( First-Person Shooter, FPS)游戏。在FPS游戏中,玩家(士兵)共享一些状态,如外在表现和行为。例如,在《反恐精英》游戏中,同一团队(反恐精英或恐怖分子)的所有士兵看起来都是一样的(外在表现)。同一个游戏中,(两个团队的)所有士兵都有一些共同的动作,比如,跳起、低头等(行为)。这意味着我们可以创建一个享元来包含所有共

同的数据。当然,士兵也有许多因人而异的可变数据,这些数据不是享元的一部分,比如,枪支、

健康状况和地理位置等。

1. 现实生活中的例子

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

2. 软件的例子

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

3. 应用案例

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

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

  • [ ] 应用需要使用大量的对象。
  • [ ] 对象太多,存储/渲染它们的代价太大。一旦移除对象中的可变状态(因为在需要之时,应

    该由客户端代码显式地传递给享元),多组不同的对象可被相对更少的共享对象所替代。
  • [ ] 对象ID对于应用不重要。对象共享会造成ID比较的失败,所以不能依赖对象ID(那些在

    客户端代码看来不同的对象,最终具有相同的ID )。
4. 实现

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

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

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

在Python中,享元可以以多种方式实现,但我发现这个例子中展示的实现非常简洁。 pool变量是一个对象池(换句话说,是我们的缓存)。注意: pool是一个类属性(类的所有实例共享的一个变量。使用特殊方法__new__(这个方法在__init__之前被调用),我们把Tree类变换成一个元类,元类支持自引用。这意味着cls引用的是Tree类(请参考[ Lott14,第99页])。当客户端要创建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(t5), id(t6), id(t5) == id(t6)))

下面完整的代码清单 :

import random
from enum import Enum
TreeType = Enum('TreeType', 'apple_tree cherry_tree peach_tree')
class Tree:
pool = dict()
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
def render(self, age, x, y):
print('render a tree of type {} and age {} at ({}, {})'.format(self.tree_type,age, x, y))
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(t5), id(t6), id(t5) == id(t6))) if __name__ == '__main__':
main()

执行上面的示例程序会显示被渲染对象的类型、随机年龄以及坐标,还有相同/不同家族享元对象ID的比较结果。你在执行这个程序时别指望能看到与下面相同的输出,因为年龄和坐标是随机的,对象ID也依赖内存映射。

输出:

render a tree of type TreeType.apple_tree and age 4 at (88, 19)
render a tree of type TreeType.apple_tree and age 18 at (31, 35)
render a tree of type TreeType.apple_tree and age 7 at (54, 23)
render a tree of type TreeType.apple_tree and age 3 at (9, 11)
render a tree of type TreeType.apple_tree and age 2 at (93, 6)
render a tree of type TreeType.apple_tree and age 12 at (3, 49)
render a tree of type TreeType.apple_tree and age 10 at (5, 65)
render a tree of type TreeType.apple_tree and age 6 at (19, 16)
render a tree of type TreeType.apple_tree and age 2 at (21, 32)
render a tree of type TreeType.apple_tree and age 21 at (87, 79)
render a tree of type TreeType.cherry_tree and age 24 at (94, 31)
render a tree of type TreeType.cherry_tree and age 14 at (92, 37)
render a tree of type TreeType.cherry_tree and age 14 at (9, 88)
render a tree of type TreeType.peach_tree and age 23 at (44, 90)
render a tree of type TreeType.peach_tree and age 16 at (15, 59)
render a tree of type TreeType.peach_tree and age 1 at (81, 98)
render a tree of type TreeType.peach_tree and age 13 at (67, 63)
render a tree of type TreeType.peach_tree and age 12 at (69, 42)
trees rendered: 18
trees actually created: 3
140322427827480 == 140322427827480? True
140322427827480 == 140322427709088? False
5. 小结

本章中,我们学习了享元模式。在我们想要优化内存使用提高应用性能之时,可以使用享元。在所有内存受限(想一想嵌入式系统)或关注性能的系统(比如图形软件和电子游戏)中,这一点相当重要。基于GTK+的Exaile音乐播放器使用享元来避免对象复制, Peppy文本编辑器则使用享元来共享状态栏的属性。

一般来说,在应用需要创建大量的计算代价大但共享许多属性的对象时,可以使用享元。重点在于将不可变(可共享)的属性与可变的属性区分开。我们实现了一个树渲染器,支持三种不同的树家族。通过显式地向render()方法提供可变的年龄和x, y属性,我们成功地仅创建了3个不同的对象,而不是18个。

python设计模式之享元模式的更多相关文章

  1. 乐在其中设计模式(C#) - 享元模式(Flyweight Pattern)

    原文:乐在其中设计模式(C#) - 享元模式(Flyweight Pattern) [索引页][源码下载] 乐在其中设计模式(C#) - 享元模式(Flyweight Pattern) 作者:weba ...

  2. python 设计模式之享元(Flyweight)模式

    #写在前面 这个设计模式理解起来很容易.百度百科上说的有点绕口. #享元模式的定义 运用共享技术来有効地支持大量细粒度对象的复用. 它通过共享已经存在的对橡大幅度减少需要创建的对象数量.避免大量相似类 ...

  3. 【GOF23设计模式】享元模式

    来源:http://www.bjsxt.com/ 一.[GOF23设计模式]_享元模式.享元池.内部状态.外部状态.线程池.连接池 package com.test.flyweight; /** * ...

  4. 设计模式之享元模式(Flyweight)摘录

    23种GOF设计模式一般分为三大类:创建型模式.结构型模式.行为模式. 创建型模式抽象了实例化过程,它们帮助一个系统独立于怎样创建.组合和表示它的那些对象.一个类创建型模式使用继承改变被实例化的类,而 ...

  5. Head First设计模式之享元模式(蝇量模式)

    一.定义 享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能.这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式. ...

  6. 【Unity3D与23种设计模式】享元模式(Flyweight)

    GoF中定义: "使用共享的方式,让一大群小规模对象能更有效地运行" 享元模式一般应用在游戏角色属性设置上 游戏策划需要通过"公式计算"或者"实际测试 ...

  7. Java进阶篇设计模式之七 ----- 享元模式和代理模式

    前言 在上一篇中我们学习了结构型模式的组合模式和过滤器模式.本篇则来学习下结构型模式最后的两个模式, 享元模式和代理模式. 享元模式 简介 享元模式主要用于减少创建对象的数量,以减少内存占用和提高性能 ...

  8. Java设计模式之七 ----- 享元模式和代理模式

    前言 在上一篇中我们学习了结构型模式的组合模式和过滤器模式.本篇则来学习下结构型模式最后的两个模式, 享元模式和代理模式. 享元模式 简介 享元模式主要用于减少创建对象的数量,以减少内存占用和提高性能 ...

  9. 【设计模式】享元模式(Flyweight)

    摘要: 1.本文将详细介绍享元模式的原理和实际代码中特别是Android系统代码中的应用. 纲要: 1. 引入享元模式 2. 享元模式的概念及优缺点介绍 3. 享元模式在Android源码中的应用 1 ...

随机推荐

  1. vue :关于引用jquery的二三问题

    webpack版本:3.6.0 首先是引用jquery. 有两个地方要改. 1 (项目地址)/build/webpack.base.conf.js 2 (项目地址)/src/main.js webpa ...

  2. 【mysql】- 事务隔离级别和MVCC篇

    概念 术语 脏写( Dirty Write ): 如果一个事务修改了另一个未提交事务修改过的数据,那就意味着发了脏写 脏读( Dirty Read ) : 如果一个事务读到了另一个未提交事务修改过的数 ...

  3. 学完自动化测试,用小技能做了点兼职刷弹幕,小赚10W

    大家好,今天又给大家带来了Python爬虫的分享,继续来研究一下虎牙平台的爬虫. 起因 我冒出有一个很有趣的想法,就是,我们可以使用selenium来完成虎牙自动化登录,并且自动给主播发送弹幕功能的程 ...

  4. vue-methods三种调用的形势

    var btn = { template:`<button>组件add</button>` } var any = new Vue({ el: '#app', data:{ a ...

  5. 解决android studio Gradle无法同步问题

    打开根目录build.gradle buildscript { repositories { // 添加阿里云 maven 地址 maven { url 'http://maven.aliyun.co ...

  6. ES搜索引擎-一篇文章就够了

    toc: true title: 一周一个中间件-ES搜索引擎 date: 2019-09-19 18:43:36 tags: - 中间件 - 搜索引擎 前言 在众多搜索引擎中,solr,es是我所知 ...

  7. Laravel 使用阿里云 oss 存储对象

    一.下载安装 composer require jacobcyl/ali-oss-storage 二.注册服务提供者 在config/app.php的providers下添加: //阿里云OSS对象存 ...

  8. C#winform将dll封装到exe当中

    我们在在winform程序时经常会用到外部dll,正常情况下,我的exe运行文件旁就需要这些dll文件相伴,总感觉不爽~~特别是要把软件给别人的时候,如果DLL比较多或者没有放在同一个地方,那麻烦大了 ...

  9. Microsoft Cloud App Security 微软的云应用安全

    1.概述 微软2015年收购的一家云安全创业公司 Adallom 正式推出产品,同时更名为微软 Cloud App Security.Adallom 成立于 2012年,是一家 SaaS 云安全公司, ...

  10. 呕心搜集总结的15个“swoole”常见问题(一)

    一.升级Swoole版本 可以使用 pecl 进行安装和升级 pecl upgrade swoole 也可以直接从 github/gitee/pecl 下载一个新版本,重新安装编译. 更新 Swool ...