文章目录

Python __dict__与__slots__在属性存储上的内存差异

发布于 2026-04-29 20:24:36 · 浏览 4 次 · 评论 0 条

Python dictslots在属性存储上的内存差异

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__ 时需格外小心,否则可能导致优化失效。

遵循以下规则以避免内存浪费:

  1. 父类定义了 __slots__:子类如果不定义 __slots__,那么子类的实例将自动拥有 __dict__,导致优化失效。
  2. 父子类都定义了 __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 性能优化中极具性价比的手段。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文