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. 学会DevOps 能拿多少工资?DevOps 怎么自学?

    落地高薪!DevOps为何受宠? DevOps在近几年的发展势头可谓是迅猛无比,已经有越来越多的企业机构开始尝试落地,从国外的微软谷歌到国内的阿里腾讯,DevOps已经从时髦概念落地最佳实践,进而改变 ...

  2. 3.TCP协议

    一.TCP协议特点和报文段格式 面向连接的传输层协议 每一条TCP连接只能有两个端点 TCP提供可靠交付的服务,无差错,不丢失,不重复,按序到达 全双工通信 -> 发送缓冲:准备发送的数据&am ...

  3. 设计模式:state模式

    核心: 把状态的判断逻辑转移到表示不同状态的一系列类当中,可以把复杂的判断逻辑简化 例子: class State //状态接口 { public: virtual void show() = 0; ...

  4. Java中的大数值使用

    在Java中,偶尔会遇到超大数值,超出了已有的int,double,float等等你已知的整数.浮点数范围,那么可以使用java.math包中的两个类:BigInteger和BigDecimal. 这 ...

  5. 读懂操作系统之快表(TLB)原理(七)

    前言 前不久.我们详细分析了TLB基本原理,本节我们通过一个简单的示例再次叙述TLB的算法和原理,希望借此示例能加深我们对TLB(又称之为快表,深入理解计算机系统(第三版)又称之为翻译后备缓冲区)的理 ...

  6. 郭神的关于git软件和http的文章

    https://blog.csdn.net/guolin_blog/article/details/17482095

  7. kafka笔记——入门介绍

    中文文档 目录 kafka的优势 首先几个概念 kafka的四大核心API kafka的基本术语 主题和日志(Topic和Log) 每个分区都是一个顺序的,不可变的队列,并且可以持续的添加,分区中的每 ...

  8. ken桑带你读源码 之scrapy scrapy\extensions

    logstats.py 爬虫启动时 打印抓取网页数   item数 memdebug.py 爬虫结束 统计还被引用的内存 也就是说gc 回收不了的内存   memusage.py 监控爬虫 内存占用  ...

  9. Django开发之ORM批量操作

    版本 1 Python 3.8.2 2 Django 3.0.6 批量入库 场景: 前端页面通过 textarea 文本框提交一列多行数据到Django后台,后台通过ORM做入库操作 表名: Tabl ...

  10. PHP开发者该知道的多进程消费队列

    引言 最近开发一个小功能,用到了队列mcq,启动一个进程消费队列数据,后边发现一个进程处理不过来了,又加了一个进程,过了段时间又处理不过来了… 这种方式每次都要修改crontab,如果进程挂掉了,不会 ...