文章目录

Python __slots__对内存占用的优化原理

发布于 2026-05-30 16:22:00 · 浏览 26 次 · 评论 0 条

Python __slots__ 对内存占用的优化原理

在创建海量对象(例如百万级实例)时,你可能会发现程序占用的内存超乎预期。这往往是因为Python默认的每个实例都持有一份独立的属性字典。理解并正确使用__slots__,能帮你节省大量内存,让程序运行得更轻快。

一、 原理:理解默认的属性字典

在标准的Python类中,每个实例的属性并非直接存储在对象本身,而是存储在一个被称为属性字典__dict__)的独立结构中。

观察 一个常规类的内存布局:

class RegularPoint:
    def __init__(self, x, y):
        self.x = x
        self.y = y

# 创建两个实例
p1 = RegularPoint(1, 2)
p2 = RegularPoint(3, 4)

# 每个实例都有自己独立的属性字典
print(p1.__dict__)  # 输出: {'x': 1, 'y': 2}
print(p2.__dict__)  # 输出: {'x': 3, 'y': 4}

问题 就在于,每个RegularPoint实例(p1, p2)除了存储xy的值,都额外维护了一个完整的字典对象来存储这些属性。字典本身是一种为了灵活而牺牲内存效率的数据结构。当创建成千上万个这样的实例时,这些字典带来的内存开销会非常巨大。

二、 核心:使用 __slots__ 固化属性

__slots__的出现,就是为了解决上述内存浪费问题。它在类定义中通过一个元组(或列表)预先声明该类实例所能拥有的所有属性

声明 一个使用__slots__的类:

class SlottedPoint:
    __slots__ = (‘x‘, ‘y‘)  # 关键步骤:声明允许的属性名

    def __init__(self, x, y):
        self.x = x
        self.y = y

# 创建实例
sp1 = SlottedPoint(1, 2)
sp2 = SlottedPoint(3, 4)

关键变化 发生在内存布局上:

  1. 移除__dict__:实例不再拥有__dict__属性。尝试访问sp1.__dict__会引发AttributeError
  2. 属性直接存储:属性xy的值,被直接存储在实例对象本身的内存结构中,通常通过描述符机制实现,避免了额外字典的开销。
  3. 属性列表锁定:你只能给实例赋值__slots__元组中声明的属性。尝试赋值一个未声明的属性(如sp1.z = 3)会立即报错。这虽然牺牲了一些灵活性,但换来了内存和访问速度的优化。

三、 实验:量化内存节省效果

理论不如实践。我们可以通过sys.getsizeof和一个简单循环来直观对比两种模式的内存占用。

执行 以下代码来测量内存:

import sys

class RegularPoint:
    def __init__(self, x, y):
        self.x = x
        self.y = y

class SlottedPoint:
    __slots__ = (‘x‘, ‘y‘)
    def __init__(self, x, y):
        self.x = x
        self.y = y

# 测量单个实例的基础内存占用(不包括其属性字典的开销)
regular_size = sys.getsizeof(RegularPoint(0, 0))
slotted_size = sys.getsizeof(SlottedPoint(0, 0))

print(f“单个实例基础对象大小:“)
print(f“  常规类 (RegularPoint): {regular_size} 字节“)
print(f“  带slots类 (SlottedPoint): {slotted_size} 字节“)

# 测量批量创建实例后的总内存估算(通过字典推导式的大小来间接感受)
import tracemalloc

def measure_memory(Class, n):
    tracemalloc.start()
    instances = [Class(i, i) for i in range(n)]
    current, peak = tracemalloc.get_traced_memory()
    tracemalloc.stop()
    return current

N = 100000
mem_regular = measure_memory(RegularPoint, N)
mem_slotted = measure_memory(SlottedPoint, N)

print(f“\n创建 {N} 个实例的近似内存占用:“)
print(f“  常规类: {mem_regular / 1024:.2f} KB“)
print(f“  带slots类: {mem_slotted / 1024:.2f} KB“)

预期结果 会清晰地显示:SlottedPoint实例的基础对象大小小于RegularPoint,并且在创建大量实例时,总体内存占用有显著下降。下降幅度取决于属性数量和实例总数,有时可达到40%-50%。

四、 进阶:理解混合模式与继承

在使用__slots__时,有几个关键规则需要牢记,否则优化可能失效。

规则1:父类与子类

  • 如果父类没有定义__slots__,那么即使子类定义了__slots__,子类实例依然会拥有__dict__,优化效果大打折扣。
  • 最佳实践是,从不使用任何属性字典的基类(可以是一个只有__slots__的空类)开始继承。
class Base:
    __slots__ = () # 空基类,作为优化的起点

class Derived(Base):
    __slots__ = (‘value‘,)

d = Derived()
d.value = 10
# d.__dict__ 会报错,优化生效

规则2:__slots____dict__ 的混合
你可以在__slots__列表中包含‘__dict__‘。这会让实例同时拥有固定的槽位和一个属性字典。这是一种折中方案,既为常用属性优化内存,又保留了动态添加其他属性的灵活性。

规则3:弱引用支持
如果希望实例能被弱引用(weakref),你需要在__slots__元组中显式加入‘__weakref__‘

import weakref

class SlottedWithWeakref:
    __slots__ = (‘x‘, ‘__weakref__‘)

obj = SlottedWithWeakref()
obj.x = 1
ref = weakref.ref(obj) # 现在可以正常创建弱引用

五、 应用:何时使用 __slots__

__slots__并非万能药,应明智使用。

推荐使用场景

  1. 需要创建大量实例:这是最主要的场景。例如,游戏中的子弹、粒子系统,数据处理中的数据点、记录对象,Web服务中的请求上下文对象等。
  2. 属性固定且已知:类的属性集合在设计时就完全确定,未来不需要动态添加。
  3. 对内存敏感的环境:嵌入式设备、移动应用或需要处理超大规模数据的后台服务。

谨慎或避免使用场景

  1. 需要动态添加属性:比如ORM对象、某些需要灵活性的元编程场景。
  2. 属性会变化:类的设计可能在未来需要添加新属性。
  3. 频繁使用多重继承__slots__在多重继承中的行为较为复杂,容易出错。

最后一步:在你的项目中,定位创建对象最密集、数量最庞大的类。分析其属性是否固定。如果满足条件,应用 __slots__测量优化前后的内存占用变化。

评论 (0)

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

扫一扫,手机查看

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