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. The Prices

    题目描述 你要购买\(m\)种物品各一件,一共有\(n\)家商店,你到第\(i\)家商店的路费为\(d[i]\),在第家商店购买第\(j\)种物品的费用为\(c[i][j]\),求最小总费用. 输入格 ...

  2. 关于 iframe 的小问题若干

    我们知道,iframe在传统的MVC项目里是个很常用的东西. 但这玩意用起来有时会有点烦人. 比如说:我有个一个页面套了一个iframe,iframe里面的页面通过a标签来切换.怎么做? <li ...

  3. 一文入门DNS?从访问GitHub开始

    前言 大家都是做开发的,都有GitHub的账号,在日常使用中肯定会遇到这种情况,在不修改任何配置的情况下,有时可以正常访问GitHub,有时又直接未响应,来一起捋捋到底是为啥. GitHub访问的千层 ...

  4. Django序列化组件Serializers详解

    本文主要系统性的讲解django rest framwork 序列化组件的使用,基本看完可以解决工作中序列化90%的问题,写作参考官方文档https://www.django-rest-framewo ...

  5. 干货 | 这可能全网最好的BatchNorm详解

    文章来自:公众号[机器学习炼丹术].求关注~ 其实关于BN层,我在之前的文章"梯度爆炸"那一篇中已经涉及到了,但是鉴于面试经历中多次问道这个,这里再做一个更加全面的讲解. Inte ...

  6. SpringCloud系列使用Eureka进行服务治理

    1. 什么是微服务? "微服务"一词来自国外的一篇博文,网站:https://martinfowler.com/articles/microservices.html 如果您不能看 ...

  7. VulnHub靶场学习_HA: Pandavas

    HA: Pandavas Vulnhub靶场 下载地址:https://www.vulnhub.com/entry/ha-pandavas,487/ 背景: Pandavas are the warr ...

  8. html实现a元素href的URL链接自动刷新或新窗口打开

    有时我们想实现这样一个功能,点击一个链接,如果这个链接浏览器已经打开过,则刷新已经打开的链接窗口:如果这个链接没有打开过,则使用新窗口打开这个链接页面. 这是一个非常好的体验增强功能,可以有效避免浏览 ...

  9. C++程序员容易走入性能优化误区!对此你怎么看呢?

    有些C++ 程序员,特别是只写C++ 没有写过 Python/PHP 等慢语言的程序员,容易对性能有心智负担,就像着了魔一样,每写3 行代码必有一行代码因为性能考虑而优化使得代码变形(复杂而晦涩). ...

  10. 牛客练习赛60 D 斩杀线计算大师

    LINK:斩杀线计算大师 给出a,b,c三个值 求出 ax+by+cz=k的x,y,z的正整数解 保证一定有解. 考虑两个数的时候 ax+by=k 扩展欧几里得可以解决. 三个数的时候 一个暴力的想法 ...