Python dict与slots在属性存储上的内存差异
Python 作为一门动态语言,其灵活性允许我们在运行时随意给对象添加属性。这种便利性背后,是 Python 默认为每个对象维护的一个字典 __dict__。然而,当需要创建成千上万个对象时,这种默认机制会消耗大量内存。__slots__ 是 Python 提供的一种优化方案,通过牺牲一部分灵活性来显著降低内存占用。
以下步骤将带你从默认机制入手,逐步掌握如何使用 __slots__ 优化内存,并量化对比两者差异。
1. 理解默认存储机制 __dict__
默认情况下,Python 对象使用一个字典来存储其属性。字典基于哈希表实现,虽然查询速度快,但内存开销较大,因为字典需要维护额外的哈希表结构以处理冲突和扩容。
创建一个普通的 Python 类并实例化,观察其内部结构。
输入以下代码:
class NormalUser:
def __init__(self, name, age):
self.name = name
self.age = age
user = NormalUser("Alice", 30)
# 查看对象的属性字典
print(user.__dict__)
运行代码后,控制台会输出:{'name': 'Alice', 'age': 30}。
这表明对象 user 内部持有一个完整的字典。字典本身除了存储键值对,还需要预留内存空间以保证哈希表的效率。对于单个对象,这种开销微乎其微;但如果创建一百万个对象,这些冗余的哈希表结构将占用大量 RAM。
2. 使用 __slots__ 优化存储
__slots__ 的作用是告诉 Python 解释器:这个类的实例只有固定的一组属性,不需要动态字典。解释器会为每个属性分配固定的空间(通常只是一个指针大小的槽位),从而消除了字典的开销。
定义一个使用 __slots__ 的类,并尝试动态添加属性以验证其限制。
输入以下代码:
class SlotUser:
# 显式声明允许的属性列表
__slots__ = ['name', 'age']
def __init__(self, name, age):
self.name = name
self.age = age
s_user = SlotUser("Bob", 25)
# 尝试添加未在 __slots__ 中声明的属性
try:
s_user.email = "bob@example.com"
except AttributeError as e:
print(f"报错信息: {e}")
运行代码,程序会抛出 AttributeError:'SlotUser' object has no attribute 'email'。
这说明 __slots__ 成功禁用了动态属性绑定。由于不再需要维护哈希表,内存结构变得极度紧凑。
3. 量化对比内存差异
为了直观展示两者在内存上的差距,我们使用 sys 模块分别创建大量对象,并测量它们占用的内存大小。
注意:sys.getsizeof() 只返回对象自身占用的内存,对于 NormalUser,我们需要手动加上 __dict__ 的大小,才能反映真实的内存消耗。
输入以下测试代码:
import sys
class NormalUser:
__slots__ = []
def __init__(self):
# 动态创建属性,模拟真实场景
self.id = 1001
self.data = "x" * 100
class SlotUser:
__slots__ = ['id', 'data']
def __init__(self):
self.id = 1001
self.data = "x" * 100
def get_size(obj):
"""获取对象及其属性字典的真实内存占用"""
size = sys.getsizeof(obj)
if hasattr(obj, '__dict__'):
size += sys.getsizeof(obj.__dict__)
# 加上字典中引用的值的大小(近似值,不包含字符串内容本身)
for key, value in obj.__dict__.items():
size += sys.getsizeof(key) + sys.getsizeof(value)
else:
# 对于 slots,属性直接存储在对象结构中或描述器中
# 这里简单计算对象自身大小,属性指针通常包含在基础大小中或略有增加
for slot in obj.__slots__:
try:
size += sys.getsizeof(getattr(obj, slot))
except AttributeError:
pass
return size
n_user = NormalUser()
s_user = SlotUser()
n_size = get_size(n_user)
s_size = get_size(s_user)
print(f"NormalUser 内存占用: {n_size} 字节")
print(f"SlotUser 内存占用: {s_size} 字节")
print(f"内存节省比例: {(n_size - s_size) / n_size * 100:.2f}%")
执行上述代码。根据 Python 版本和操作系统的不同(32位或64位),具体数值会有所波动,但趋势是一致的。
下表展示了在典型 64 位 Python 环境下的对比结果(数值仅为示例,具体以实际运行为准):
| 特性 | NormalUser (使用 dict) | SlotUser (使用 slots) |
|---|---|---|
| 对象自身开销 | 较大(包含字典指针) | 极小(仅包含头部信息) |
| 属性存储结构 | 哈希表 | 数组/偏移量 |
| 动态添加属性 | 支持 | 不支持 |
| 单个对象总内存 | ~560 字节 | ~320 字节 |
| 内存占用比 | 100% | ~57% |
4. 掌握继承中的 __slots__ 规则
在继承关系中使用 __slots__ 时需格外小心,否则可能导致优化失效。
遵循以下规则以避免内存浪费:
- 父类定义了
__slots__:子类如果不定义__slots__,那么子类的实例将自动拥有__dict__,导致优化失效。 - 父子类都定义了
__slots__:子类的__slots__会包含父类的__slots__。重复定义父类已包含的属性名不会报错,但属于冗余操作。
输入以下代码验证继承场景:
class Base:
__slots__ = ['a']
# 错误示范:子类未定义 __slots__,导致退化为动态字典
class ChildWrong(Base):
def __init__(self):
self.a = 1
self.b = 2 # 动态添加,内存泄漏
# 正确示范:子类也定义 __slots__
class ChildRight(Base):
__slots__ = ['b'] # 只需声明新增的属性
def __init__(self):
self.a = 1
self.b = 2
c_wrong = ChildWrong()
c_right = ChildRight()
print(f"Wrong has __dict__: {hasattr(c_wrong, '__dict__')}") # 输出 True
print(f"Right has __dict__: {hasattr(c_right, '__dict__')}") # 输出 False
5. 选择合适的应用场景
并非所有类都适合使用 __slots__。根据以下决策逻辑选择是否启用优化。
仅在以下情况使用 __slots__:
- 需要实例化成千上万个该类的对象(如数据库记录、粒子系统、图节点)。
- 类的属性在实例化后基本固定,不需要动态添加。
- 内存资源受限的环境(如嵌入式设备运行 Python)。
在以下情况避免使用 __slots__:
- 需要频繁动态添加或删除属性。
- 需要使用
__weakref__特性(除非显式将__weakref__加入__slots__列表)。 - 类主要用于简单的配置或数据传递,实例数量极少。
通过将属性从哈希表转换为静态数组,__slots__ 能够减少约 40% 甚至更多的内存占用,是 Python 性能优化中极具性价比的手段。

暂无评论,快来抢沙发吧!