《Python编程实战:运用设计模式、并发和程序库创建高质量程序》—— 2.6 享元模式

简介:

本节书摘来自华章出版社《Python编程实战:运用设计模式、并发和程序库创建高质量程序》一 书中的第2章,第2.6节,作者:(美) Mark Summerfield,更多章节内容可以访问云栖社区“华章计算机”公众号查看。

2.6 享元模式

如果有许多比较小的对象需要处理,而这些小对象很多又彼此相同,那么就可以使用“享元模式”(Flyweight Pattern)。该模式的实现方式为:只给每种对象创建一个实例,并在有需要时共享此实例。
由于Python使用对象引用,所以很自然地体现出了享元模式的思路。比方说,如果有个字符串列表很长,而且其中许多字符串都一样,那么,使用对象引用(也就是变量)来存储要比直接使用“字面量字符串”(literal string)存储节省许多内存。
screenshot

在上述代码片段中,x元组用8个对象引用来保存3个字符串,而y元组则用8个对象引用保存了8个字符串,因为这种简化的写法实际上与_anonymous_item0 = "red", ..., _anonymous_item7 = "green"; y = (_anonymous_item0, ... _anonymous_item7)等效。
要想在Python语言中利用享元模式,最简单的办法可能就是使用dict了,它可以把每个值都同独特的键关联起来。比方说,在创建大量HTML页面时,我们想根据CSS(Cascading Style Sheets,层叠样式表)来指定“字型”(font),这样就不用每次都创建新字型了,而是可以预先(或在首次使用时)把它们保存在dict里面。等需要使用某个字型时,再将其从dict里取出来。这样就能保证每种字型无论使用多少次,都只会创建一次。
有时我们需要处理大量对象,而其中绝大部分或所有对象都互不相同,并且这些对象未必很小。在这种情况下,有个简单的办法可以降低内存用量,这就是使用__slots__。
screenshot

上面这个简单的Point类可以存放点的三维坐标及颜色。由于用了__slots__,所以Point实例都没有自己的dict(也就是没有self.__dict__)。然而,这样做同时也意味着不能向单个对象中随意添加attribute。(该类代码节选自pointstore1.py。)
在某台电脑中测试时,用上述代码创建含有100万个点的元组(测试程序几乎没有做其他事情)需要2.5秒,并占用183MiB内存。若是不用__slots__,那么执行时间能少零点几秒,但内存占用量却高达312MiB。
默认情况下,Python总是会花更多内存来提升执行速度,但如果有必要的话,通常也可以反过来做,那就是通过降低执行速度来节省内存用量。
screenshot

上面列出了第二版Point类的前几行代码(该类节选自pointstore2.py)。它用DBM(键值对)数据库来保存数据,而数据库本身则存放在磁盘文件中。指向DBM的对象引用保存在静态的(也就是类级别的)Point.__dbm变量里。所有Point实例都使用同一份底层DBM文件。我们首先要打开DBM文件,以便后续使用。shelve模块的默认做法是:如果没发现相关的DBM文件,那就自动创建一份。(稍后我们将演示如何保证DBM文件能正常关闭。)
在存储值时,shelve模块会将其“序列化”(pickle),而在获取值时,则会将其“反序列化”(unpickle)。(由于在反序列化的过程中能够执行任意Python代码,所以Python的序列化格式是不安全的。因此,切勿使用由不可信的数据源所提供的序列化数据,也不要将无访问限制的数据序列化。如果想在这些情况下使用序列化数据,那么可通过“校验和”(checksum)及“加密”(encryption)等自制的安全措施来保证数据安全。)
screenshot

上述方法的代码与pointstore1.py中的完全相同,但这些值都会存储到底层的DBM文件里。
screenshot

上述方法可提供Point实例中x、y、z、color等attribute的“键字符串”(key string)。这个键由实例的十六进制ID(ID是由Python语言内置的id()函数所返回的独特数字)及attribute名构成。比方说,某个Point实例的ID是3?954?827,那么其x值所对应的键字符串就是“3C588B:x”,而y值则可由“3C588B:y”这个键查到,其余attribute亦是如此。
screenshot

上述方法会在访问Point对象的某个attribute时(比如执行x = point.x时)调用。
DBM数据库的键与值必须是bytes。好在Python的DBM模块可以接受str或bytes作键,遇到str时,会用默认的UTF-8编码将其转换为bytes。如果像本例一样使用shelve模块,那么凡是可以序列化的值就都能保存到数据库里,因为shelve模块会根据情况在其他类型与bytes之间相互转换。
上面两个方法使我们能够获得与attribute相关的键,并根据键来查出attribute值。另外,由于使用了shelve模块,所以获取到的值会从序列化的bytes自动转换成原本的类型(比方说,获取到的点颜色值会是int或None类型)。
screenshot

设置Point的attribute时(例如执行point.y = y时)会调用上述方法。在该方法中,我们会根据键查出相关的attribute值,并通过shelve模块把值序列化为bytes。
screenshot

在Point类最后,我们通过atexit模块的register()函数来注册DBM的close()方法,使得程序在终止时能够调用此方法。
在某台测试机上创建含有100万个点的数据库大约需要一分钟时间,但程序只占用29MiB内存(外加361MiB的磁盘文件),而第一版程序则要占用183MiB内存。尽管生成DBM文件确实需要一些时间,但只要生成好了,查询速度就会很快,因为大部分操作系统都会把频繁使用的磁盘文件缓存起来。

相关文章
|
22小时前
|
Java 测试技术 Python
Python的多线程允许在同一进程中并发执行任务
【5月更文挑战第17天】Python的多线程允许在同一进程中并发执行任务。示例1展示了创建5个线程打印"Hello World",每个线程调用同一函数并使用`join()`等待所有线程完成。示例2使用`ThreadPoolExecutor`下载网页,创建线程池处理多个URL,打印出每个网页的大小。Python多线程还可用于线程间通信和同步,如使用Queue和Lock。
12 1
|
2天前
|
设计模式 Java 开发者
【搞懂设计模式】享元模式:共享节约,皆大欢喜!
【搞懂设计模式】享元模式:共享节约,皆大欢喜!
7 0
|
2天前
|
Python
【Python进阶(二)】——程序调试方法
【Python进阶(二)】——程序调试方法
|
2天前
|
Python
Python的全局变量作用于整个程序,生命周期与程序相同,而局部变量仅限函数内部使用,随函数执行结束而销毁。
【5月更文挑战第11天】Python的全局变量作用于整个程序,生命周期与程序相同,而局部变量仅限函数内部使用,随函数执行结束而销毁。在函数内部修改全局变量需用`global`关键字声明,否则会创建新局部变量。
104 2
|
2天前
|
消息中间件 程序员 调度
Python并发编程:利用多线程提升程序性能
本文探讨了Python中的并发编程技术,重点介绍了如何利用多线程提升程序性能。通过分析多线程的原理和实现方式,以及线程间的通信和同步方法,读者可以了解如何在Python中编写高效的并发程序,提升程序的执行效率和响应速度。
|
2天前
|
缓存 Shell 开发工具
[oeasy]python0016_在vim中直接运行python程序
在 Vim 编辑器中,可以通过`:!`命令来执行外部程序,例如`:!python3 oeasy.py`来运行Python程序。如果想在不退出Vim的情况下运行当前编辑的Python文件,可以使用`%`符号代表当前文件名,所以`:!python3 %`同样能运行程序。此外,可以使用`|`符号连续执行命令,例如`:w|!python3 %`会先保存文件(`w`)然后运行Python程序。这样,就可以在不离开Vim的情况下完成编辑、保存和运行Python程序的流程。
19 0
|
2天前
|
监控 开发者 Python
Python中记录程序报错信息的实践指南
Python中记录程序报错信息的实践指南
17 1
|
2天前
|
监控 测试技术 持续交付
Python自动化测试代理程序可用性
总之,通过编写测试用例、自动化测试和设置监控系统,您可以确保Python自动化测试代理程序的可用性,并及时发现和解决问题。这有助于提供更可靠和高性能的代理服务。
17 4
|
2天前
|
监控 测试技术 API
Python Web应用程序构建
【4月更文挑战第11天】Python Web开发涉及多种框架,如Django、Flask和FastAPI,选择合适框架是成功的关键。示例展示了使用Flask创建简单Web应用,以及如何使用ORM(如SQLAlchemy)管理数据库。
59260 4
|
2天前
|
人工智能 数据库 开发者
Python中的atexit模块:优雅地处理程序退出
Python中的atexit模块:优雅地处理程序退出
19 3