- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- Python 术语表
- Python 版本表
- 排版约定
- 使用代码示例
- 第一部分 序幕
- 第 1 章 Python 数据模型
- 第二部分 数据结构
- 第 2 章 序列构成的数组
- 第 3 章 字典和集合
- 第 4 章 文本和字节序列
- 第三部分 把函数视作对象
- 第 5 章 一等函数
- 第 6 章 使用一等函数实现设计模式
- 第 7 章 函数装饰器和闭包
- 第四部分 面向对象惯用法
- 第 8 章 对象引用、可变性和垃圾回收
- 第 9 章 符合 Python 风格的对象
- 第 10 章 序列的修改、散列和切片
- 第 11 章 接口:从协议到抽象基类
- 第 12 章 继承的优缺点
- 第 13 章 正确重载运算符
- 第五部分 控制流程
- 第 14 章 可迭代的对象、迭代器和生成器
- 14.1 Sentence 类第1版:单词序列
- 14.2 可迭代的对象与迭代器的对比
- 14.3 Sentence 类第2版:典型的迭代器
- 14.4 Sentence 类第3版:生成器函数
- 14.5 Sentence 类第4版:惰性实现
- 14.6 Sentence 类第5版:生成器表达式
- 14.7 何时使用生成器表达式
- 14.8 另一个示例:等差数列生成器
- 14.9 标准库中的生成器函数
- 14.10 Python 3.3 中新出现的句法:yield from
- 14.11 可迭代的归约函数
- 14.12 深入分析 iter 函数
- 14.13 案例分析:在数据库转换工具中使用生成器
- 14.14 把生成器当成协程
- 14.15 本章小结
- 14.16 延伸阅读
- 第 15 章 上下文管理器和 else 块
- 第 16 章 协程
- 第 17 章 使用期物处理并发
- 第 18 章 使用 asyncio 包处理并发
- 第六部分 元编程
- 第 19 章 动态属性和特性
- 第 20 章 属性描述符
- 第 21 章 类元编程
- 结语
- 延伸阅读
- 附录 A 辅助脚本
- Python 术语表
- 作者简介
- 关于封面
9.8 使用 __slots__ 类属性节省空间
默认情况下,Python 在各个实例中名为 __dict__ 的字典里存储实例属性。如 3.9.3 节所述,为了使用底层的散列表提升访问速度,字典会消耗大量内存。如果要处理数百万个属性不多的实例,通过 __slots__ 类属性,能节省大量内存,方法是让解释器在元组中存储实例属性,而不用字典。
继承自超类的 __slots__ 属性没有效果。Python 只会使用各个类中定义的 __slots__ 属性。
定义 __slots__ 的方式是,创建一个类属性,使用 __slots__ 这个名字,并把它的值设为一个字符串构成的可迭代对象,其中各个元素表示各个实例属性。我喜欢使用元组,因为这样定义的 __slots__ 中所含的信息不会变化,如示例 9-11 所示。
示例 9-11 vector2d_v3_slots.py:只在 Vector2d 类中添加了 __slots__ 属性
class Vector2d: __slots__ = ('__x', '__y') typecode = 'd' # 下面是各个方法(因排版需要而省略了)
在类中定义 __slots__ 属性的目的是告诉解释器:“这个类中的所有实例属性都在这儿了!”这样,Python 会在各个实例中使用类似元组的结构存储实例变量,从而避免使用消耗内存的 __dict__ 属性。如果有数百万个实例同时活动,这样做能节省大量内存。
如果要处理数百万个数值对象,应该使用 NumPy 数组(参见 2.9.3 节)。NumPy 数组能高效使用内存,而且提供了高度优化的数值处理函数,其中很多都一次操作整个数组。我定义 Vector2d 类的目的是讨论特殊方法,因为我不太想随便举些例子。
在示例 9-12 中,我们运行了两个构建列表的脚本,这两个脚本都使用列表推导创建 10 000 000 个 Vector2d 实例。mem_test.py 脚本的命令行参数是一个模块的名字,模块中定义了不同版本的 Vector2d 类。第一次运行使用的是 vector2d_v3.Vector2d 类(在示例 9-7 中定义),第二次运行使用的是定义了 __slots__ 的 vector2d_v3_slots.Vector2d 类。
示例 9-12 mem_test.py 使用指定模块(如 vector2d_v3.py)中定义的 Vector2d 类创建 10 000 000 个实例
$ time python3 mem_test.py vector2d_v3.py Selected Vector2d type: vector2d_v3.Vector2d Creating 10,000,000 Vector2d instances Initial RAM usage: 5,623,808 Final RAM usage: 1,558,482,944 real 0m16.721s user 0m15.568s sys 0m1.149s $ time python3 mem_test.py vector2d_v3_slots.py Selected Vector2d type: vector2d_v3_slots.Vector2d Creating 10,000,000 Vector2d instances Initial RAM usage: 5,718,016 Final RAM usage: 655,466,496 real 0m13.605s user 0m13.163s sys 0m0.434s
如示例 9-12 所示,在 10 000 000 个 Vector2d 实例中使用 __dict__ 属性时,RAM 用量高达 1.5GB;而在 Vector2d 类中定义 __slots__ 属性之后,RAM 用量降到了 655MB。此外,定义了 __slots__ 属性的版本运行速度也更快。这个测试中使用的 mem_test.py 脚本其实只用于加载一个模块、检查内存用量和格式化结果,所用的代码与本章没有太大关联,因此放入附录 A 中的示例 A-4 里。
在类中定义 __slots__ 属性之后,实例不能再有 __slots__ 中所列名称之外的其他属性。这只是一个副作用,不是 __slots__ 存在的真正原因。不要使用 __slots__ 属性禁止类的用户新增实例属性。__slots__ 是用于优化的,不是为了约束程序员。
然而,“节省的内存也可能被再次吃掉”:如果把 '__dict__' 这个名称添加到 __slots__ 中,实例会在元组中保存各个实例的属性,此外还支持动态创建属性,这些属性存储在常规的 __dict__ 中。当然,把 '__dict__' 添加到 __slots__ 中可能完全违背了初衷,这取决于各个实例的静态属性和动态属性的数量及其用法。粗心的优化甚至比提早优化还糟糕。
此外,还有一个实例属性可能需要注意,即 __weakref__ 属性,为了让对象支持弱引用(参见 8.6 节),必须有这个属性。用户定义的类中默认就有 __weakref__ 属性。可是,如果类中定义了 __slots__ 属性,而且想把实例作为弱引用的目标,那么要把 '__weakref__' 添加到 __slots__ 中。
综上,__slots__ 属性有些需要注意的地方,而且不能滥用,不能使用它限制用户能赋值的属性。处理列表数据时 __slots__ 属性最有用,例如模式固定的数据库记录,以及特大型数据集。然而,如果你经常处理大量数据,一定要了解一下 NumPy;此外,数据分析库 pandas也值得了解,这个库可以处理非数值数据,而且能导入 / 导出很多不同的列表数据格式。
__slots__ 的问题
总之,如果使用得当,__slots__ 能显著节省内存,不过有几点要注意。
每个子类都要定义 __slots__ 属性,因为解释器会忽略继承的 __slots__ 属性。
实例只能拥有 __slots__ 中列出的属性,除非把 '__dict__' 加入 __slots__ 中(这样做就失去了节省内存的功效)。
如果不把 '__weakref__' 加入 __slots__,实例就不能作为弱引用的目标。
如果你的程序不用处理数百万个实例,或许不值得费劲去创建不寻常的类,那就禁止它创建动态属性或者不支持弱引用。与其他优化措施一样,仅当权衡当下的需求并仔细搜集资料后证明确实有必要时,才应该使用 __slots__ 属性。
本章最后一个话题讨论如何在实例和子类中覆盖类属性。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论