Python `__slots__` 进阶指南:不止于节省内存,从原理到实践

ops/2024/12/18 9:41:34/

相信不少 Python 开发者都听说过 __slots__,知道它可以帮助节省内存。但你是否思考过它背后的原理,以及在实际开发中的其他妙用?让我们一起深入探讨。

从一个性能问题说起

假设你的一个系统需要处理大量的订单对象:

python">class Order:def __init__(self, order_id, symbol, price, quantity):self.order_id = order_idself.symbol = symbolself.price = priceself.quantity = quantity# 创建100万个订单对象
orders = [Order(i, "BTC", 30000, 1) for i in range(1_000_000)]

看起来很普通的代码,但当你用内存分析工具一看,这些对象占用的内存可能远超预期。为什么?

__dict__ 的开销

在 Python 中,普通类的实例属性都存储在 __dict__ 字典中。这种设计非常灵活,允许我们动态添加属性:

python">order = Order(1, "BTC", 30000, 1)
order.new_field = "动态添加的字段"  # 完全合法

但这种灵活性是有代价的:

  1. 每个实例都要维护一个字典
  2. 字典本身为了支持快速查找,会预分配一定的空间
  3. 字典的开销在对象数量大时会累积成可观的内存消耗

__slots__ 登场

让我们改造一下 Order 类:

python">class Order:__slots__ = ['order_id', 'symbol', 'price', 'quantity']def __init__(self, order_id, symbol, price, quantity):self.order_id = order_idself.symbol = symbolself.price = priceself.quantity = quantity

这个改动带来了什么变化?

  1. 内存占用显著降低(通常可以节省 30% 到 50% 的内存)
  2. 属性访问速度提升(因为不需要字典查找)
  3. 代码更加"显式",所有可能的属性一目了然

__slots__ 的工作原理

当我们使用 __slots__ 时,Python 会:

  1. 在类级别创建一个固定的内存布局,类似 C 语言中的结构体
  2. 不再为实例创建 __dict____weakref__ 属性(除非显式添加到 __slots__ 中)
  3. 将属性直接存储在预分配的固定大小的数组中,而不是字典里

这带来了两个直接的好处:

  1. 属性访问更快:直接通过数组偏移量访问,不需要哈希查找
  2. 内存占用更少:
    • 没有 __dict__ 的开销(每个实例至少节省一个字典的内存)
    • 属性存储更紧凑(类似 C 结构体)
    • 没有哈希表的空间预留

让我们用代码验证这些优势:

python">import sys
import time
import tracemallocclass OrderWithDict:def __init__(self, order_id, symbol, price, quantity):self.order_id = order_idself.symbol = symbolself.price = priceself.quantity = quantityclass OrderWithSlots:__slots__ = ['order_id', 'symbol', 'price', 'quantity']def __init__(self, order_id, symbol, price, quantity):self.order_id = order_idself.symbol = symbolself.price = priceself.quantity = quantitydef measure_memory_and_speed(cls, n_objects=1_000_000):# 启动内存跟踪tracemalloc.start()# 创建对象start_time = time.time()objects = [cls(i, "BTC", 30000, 1) for i in range(n_objects)]creation_time = time.time() - start_time# 测量内存current, peak = tracemalloc.get_traced_memory()tracemalloc.stop()# 测试属性访问速度start_time = time.time()for obj in objects:_ = obj.order_id_ = obj.symbol_ = obj.price_ = obj.quantityaccess_time = time.time() - start_timereturn {"内存占用(MB)": peak / 1024 / 1024,"对象创建时间(秒)": creation_time,"属性访问时间(秒)": access_time}def main():# 测试普通类print("测试普通类:")dict_results = measure_memory_and_speed(OrderWithDict)for k, v in dict_results.items():print(f"{k}: {v:.2f}")print("\n测试使用 __slots__ 的类:")slots_results = measure_memory_and_speed(OrderWithSlots)for k, v in slots_results.items():print(f"{k}: {v:.2f}")# 计算差异百分比print("\n性能提升:")for k in dict_results:improvement = (dict_results[k] - slots_results[k]) / dict_results[k] * 100print(f"{k}: 提升 {improvement:.1f}%")# 展示单个对象的大小差异normal_obj = OrderWithDict(1, "BTC", 30000, 1)slots_obj = OrderWithSlots(1, "BTC", 30000, 1)print(f"\n单个对象大小对比:")print(f"普通对象: {sys.getsizeof(normal_obj)} bytes")print(f"普通对象的__dict__: {sys.getsizeof(normal_obj.__dict__)} bytes")print(f"普通对象总大小: {sys.getsizeof(normal_obj) + sys.getsizeof(normal_obj.__dict__)} bytes")print(f"Slots对象: {sys.getsizeof(slots_obj)} bytes")try:print(f"Slots对象的__dict__: {sys.getsizeof(slots_obj.__dict__)} bytes")except AttributeError as e:print(f"Slots对象没有__dict__属性:{e}")if __name__ == "__main__":main()

输出如下:

测试普通类:
内存占用(MB): 179.71
对象创建时间(秒): 1.08
属性访问时间(秒): 0.08测试使用 __slots__ 的类:
内存占用(MB): 95.79
对象创建时间(秒): 0.67
属性访问时间(秒): 0.07性能提升:
内存占用(MB): 提升 46.7%
对象创建时间(秒): 提升 37.5%
属性访问时间(秒): 提升 4.8%单个对象大小对比:
普通对象: 48 bytes
普通对象的__dict__: 104 bytes
普通对象总大小: 152 bytes
Slots对象: 64 bytes
Slots对象没有__dict__属性:'OrderWithSlots' object has no attribute '__dict__'

这里注意到,使用了 __slots__ 的类没有 __dict__ 属性,这是因为它的属性是直接存储在数组中的。此外,直接对对象进行 sizeof 操作,是不包含其 __dict__ 的大小的。

当我们使用 sys.getsizeof() 测量单个对象大小时,它只返回对象的直接内存占用,而不包括其引用的其他对象(如 __dict__ 中存储的值)的大小。

不止于节省内存

__slots__ 除了优化性能,还能帮助我们写出更好的代码:

1. 接口契约

__slots__ 实际上定义了一个隐式的接口契约,明确告诉其他开发者,“这个类就这些属性,不多不少”:

python">class Position:__slots__ = ['symbol', 'quantity']def __init__(self, symbol, quantity):self.symbol = symbolself.quantity = quantity

这比写文档更有效 - 代码本身就是最好的文档。

2. 防止拼写错误

python">position = Position("BTC", 100)
position.quantiy = 200  # 拼写错误,会立即抛出 AttributeError

如果没有 __slots__,这个错误可能潜伏很久才被发现。

3. 更好的封装

__slots__ 天然地限制了属性的随意添加,这促使我们思考类的设计是否合理:

python">class Account:__slots__ = ['id', 'balance', '_transactions']def __init__(self, id):self.id = idself.balance = 0self._transactions = []def add_transaction(self, amount):self._transactions.append(amount)self.balance += amount

__slots__ vs @dataclass:该用谁?

既然都是用于数据类的定义,@dataclass__slots__ 是什么关系?让我们先看一个例子:

python">from dataclasses import dataclass# 普通dataclass
@dataclass
class TradeNormal:symbol: strprice: floatquantity: int# 带slots的dataclass
@dataclass
class TradeWithSlots:__slots__ = ['symbol', 'price', 'quantity']symbol: strprice: floatquantity: int# 结合使用的推荐方式
@dataclass(slots=True)  # Python 3.10+
class TradeModern:symbol: strprice: floatquantity: int

关键点解析:

  1. 默认情况@dataclass 装饰器默认不会使用 __slots__,每个实例依然会创建 __dict__

  2. Python 3.10的改进:引入了 slots=True 参数,可以自动为 dataclass 启用 __slots__

  3. 动态添加属性的陷阱

python">@dataclass
class Trade:symbol: strprice: floattrade = Trade("BTC", 30000)
trade.quantity = 1  # 可以,但会创建 __dict__@dataclass(slots=True)
class TradeLocked:symbol: strprice: floattrade_locked = TradeLocked("BTC", 30000)
trade_locked.quantity = 1  # AttributeError!

最佳实践:@dataclass__slots__ 的协同使用

  1. Python 3.10+ 的推荐用法
python">@dataclass(slots=True, frozen=True)
class Position:symbol: strquantity: int
  1. 早期Python版本的替代方案
python">@dataclass
class Position:__slots__ = ['symbol', 'quantity']symbol: strquantity: int

如何选择?

  1. 使用 @dataclass(slots=True) 的场景

    • 类的属性在定义后不会改变
    • 需要类型提示和自动生成方法
    • Python 3.10+环境
    • 注重内存效率
  2. 使用普通 @dataclass 的场景

    • 需要动态添加属性
    • 使用了某些需要 __dict__ 的库(如某些ORM)
    • Python 3.10以下版本
    • 开发阶段,类的结构还在调整
  3. 直接使用 __slots__ 的场景

    • 极致的性能要求
    • 类的结构非常简单
    • 不需要dataclass提供的额外功能

注意事项和提示

  1. 继承关系
python">@dataclass(slots=True)
class Parent:x: int@dataclass(slots=True)
class Child(Parent):y: int# Child会自动继承Parent的slots
  1. 动态属性检查
python">@dataclass(slots=True)
class Trade:symbol: strdef __setattr__(self, name, value):if name not in self.__slots__:raise AttributeError(f"Cannot add new attribute '{name}'")super().__setattr__(name, value)

此外,某些涉及动态属性的特性会受限:

python">class Frozen:__slots__ = ['x']obj = Frozen()
# 以下操作将不可用:
# vars(obj)  # TypeError: vars() argument must have __dict__ attribute
# setattr(obj, 'y', 1)  # AttributeError
  1. 性能优化建议
  • 如果确定类的结构不会改变,优先使用 @dataclass(slots=True)
  • 在性能关键的代码路径上,考虑使用性能分析工具验证收益
  • 数据类(如 DTO)且实例数量大时,用 __slots__ 是个好选择
  • 如果类的属性集合是确定的,使用 __slots__ 可以获得更好的代码质量
  • 记住:过早优化是万恶之源,先保证代码正确性和可维护性

总结

__slots__ 不仅仅是一个性能优化工具,它还能帮助我们写出更清晰、更健壮的代码。在设计数据密集型应用时,合理使用 __slots__ 可以同时获得性能和代码质量的提升。

实际工作中,可以先写普通的类,当发现性能瓶颈或需要更严格的属性控制时,再考虑引入 __slots__。毕竟,过早优化是万恶之源,而 __slots__ 的使用也确实会带来一些灵活性的损失。


http://www.ppmy.cn/ops/142873.html

相关文章

安装 telnet

参考链接 https://www.python100.com/html/80855.html Linux telnet 命令安装_failed to start telnet.service: unit not found.-CSDN博客 解决启动的问题,出问题优先看这个 安装telnet服务,以及遇到的一些坑_unit telnet.service could not be fou…

leetcode简单题数组和技巧题

数组是一种基础数据结构,可以用来处理常见的排序和二分搜索问题,典型的处理技巧包括对撞指针、滑动窗口等。 面试中的算法问题,有很多并不需要复杂的数据结构支撑,就是用数组,就能考察出很多东西。 题型1:…

6.1 初探MapReduce

MapReduce是一种分布式计算框架,用于处理大规模数据集。其核心思想是“分而治之”,通过Map阶段将任务分解为多个简单任务并行处理,然后在Reduce阶段汇总结果。MapReduce编程模型包括Map和Reduce两个阶段,数据来源和结果存储通常在…

golang 判断一个点是否在一个多边形内

我有一需求为:判断一个点(经纬度范围)是否在一个多边形范围内(多个经纬度点) 这里我借助几何库( github.com/paulmach/orb)来处理地理空间数据 可以通过在线获取经纬度来确认代码正确性 packa…

开源 AI 智能名片微信小程序在企业微信生态中的创新应用与价值拓展

摘要:本论文聚焦于企业微信这一重要的企业通信与办公工具,深入探讨开源 AI 智能名片微信小程序如何与之深度融合并发挥独特作用。分析企业微信的功能特性以及在企业内外连接方面的重要意义,阐述开源 AI 智能名片微信小程序在增强企业社交互动…

国际网络专线是什么?有什么优势?

国际网络专线作为一种独立的网络连接方式,通过卫星或海底光缆等物理链路,将全球不同国家和地区的网络直接互联,为企业提供了可靠的通信渠道。本文将详细探讨国际网络专线的优势以及其广泛的应用场景。 国际网络专线的优势解析 1. 专属连接&am…

利用 Flink 构建实时数据写入流水线:从 Paimon 到 Iceberg

在现代数据架构中,实时数据处理与可扩展的数据存储至关重要。Apache Flink 作为低延迟、高吞吐的流处理框架,可与 Paimon 和 Iceberg 等数据湖表格式无缝结合,从而构建批流一体、弹性扩展的实时数据分析平台。本文将结合命令示例与参数对比&a…

简单了解一下 Go 语言的构建约束?

​构建约束是一种在 Go 语言中控制源文件编译条件的方法,它可以让您指定某些文件只在特定的操作系统、架构、编译器或 Go 版本下编译,而在其他环境中自动忽略。这样可以方便您针对不同的平台或场景编写不同的代码,实现条件编译的功能。 构建…