当前位置:首页>python>创建一百万个 Python 对象,有没有 __slots__ 差了整整一倍内存

创建一百万个 Python 对象,有没有 __slots__ 差了整整一倍内存

  • 2026-10-11 08:26:12
创建一百万个 Python 对象,有没有 __slots__ 差了整整一倍内存

创建一百万个 Python 对象,有没有 __slots__ 差了整整一倍内存

去年做一个数据处理项目,需要把一份 CSV 文件里的几百万条记录解析成 Python 对象。

跑了一会儿,服务器内存报警了。

当时我的第一反应是:数据量太大,换成流式处理?改用 pandas?还是加机器?

结果同事过来看了一眼,加了几个字,内存占用直接砍掉了将近一半。

他加的东西叫 __slots__。


Python 对象为什么这么"重"

要理解 __slots__,得先搞清楚 Python 普通对象为什么占内存多。

Python 里每个类的实例,默认都有一个叫 __dict__ 的字典,用来存储这个实例的所有属性。

字典这个结构本身就是为了灵活性设计的——你可以随时往里添加任何键。但这灵活性有代价:字典在内存里的存储开销相当大,哪怕它只装了三个字段。

import sys

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

p = Point(1.0, 2.0, 3.0)
print(sys.getsizeof(p))           # 对象本身大小
print(sys.getsizeof(p.__dict__))  # 属性字典大小
print(p.__dict__)

输出:

48
232
{'x': 1.0, 'y': 2.0, 'z': 3.0}

注意:对象本身只有 48 字节,但它挂着的那个字典就占了 232 字节。三个浮点数,一共才 24 字节的数据,外壳就用了 232 字节。

这就是问题所在。


__slots__ 做了什么

__slots__ 告诉 Python:这个类的实例只会有这几个属性,固定住,不需要字典了。

Python 会改用更紧凑的结构(类似 C 的结构体)来存储属性,省掉那个开销巨大的 __dict__。

class PointSlots:
    __slots__ = ['x', 'y', 'z']

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

ps = PointSlots(1.0, 2.0, 3.0)
print(sys.getsizeof(ps))          # 对象本身大小
print(hasattr(ps, '__dict__'))    # 是否有 __dict__

输出:

64
False

对象本身从 48 字节变成了 64 字节(因为把属性直接内嵌进来了),但没有那个 232 字节的字典了。

总内存:48 + 232 = 280 字节 变成了 64 字节。节省了将近 77%。


百万对象,差距有多大

光看单个对象的数字不直观,来测一个真实场景:

import tracemalloc
import sys

class Record:
    def __init__(self, user_id, score, label):
        self.user_id = user_id
        self.score = score
        self.label = label

class RecordSlots:
    __slots__ = ['user_id', 'score', 'label']

    def __init__(self, user_id, score, label):
        self.user_id = user_id
        self.score = score
        self.label = label

N = 1_000_000

# 测量普通类
tracemalloc.start()
records = [Record(i, i * 0.1, i % 10) for i in range(N)]
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"普通类 - 当前内存: {current / 1024 / 1024:.1f} MB, 峰值: {peak / 1024 / 1024:.1f} MB")

# 测量 __slots__ 类
tracemalloc.start()
records_slots = [RecordSlots(i, i * 0.1, i % 10) for i in range(N)]
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"__slots__ - 当前内存: {current / 1024 / 1024:.1f} MB, 峰值: {peak / 1024 / 1024:.1f} MB")

输出:

普通类 - 当前内存: 247.3 MB, 峰值: 247.3 MB
__slots__ - 当前内存: 114.5 MB, 峰值: 114.5 MB

一百万条记录,内存差了将近 133 MB。

同样的数据,用 __slots__ 少用了 54% 的内存。


还有一个隐藏收益:属性访问更快

除了省内存,__slots__ 对象的属性访问速度也更快。

普通对象访问属性要走字典查找,而 __slots__ 的属性是固定偏移量访问,直接到内存地址取值,省去了哈希计算和冲突处理。

import timeit

p_normal = Point(1.0, 2.0, 3.0)
p_slots = PointSlots(1.0, 2.0, 3.0)

time_normal = timeit.timeit(lambda: p_normal.x + p_normal.y + p_normal.z, number=5_000_000)
time_slots = timeit.timeit(lambda: p_slots.x + p_slots.y + p_slots.z, number=5_000_000)

print(f"普通对象访问耗时: {time_normal:.3f}s")
print(f"__slots__ 访问耗时: {time_slots:.3f}s")
print(f"速度提升: {time_normal / time_slots:.2f}x")

输出:

普通对象访问耗时: 0.412s
__slots__ 访问耗时: 0.298s
速度提升: 1.38x

快了将近 40%,五百万次访问节省了 0.11 秒。看起来不多,但在高频循环里累积效果很可观。


用 __slots__ 要注意的坑

坑一:没法随意添加属性了

用了 __slots__,实例就不能动态添加不在列表里的属性:

ps = PointSlots(1.0, 2.0, 3.0)
ps.w = 4.0  # AttributeError: 'PointSlots' object has no attribute 'w'

这是限制,也是约束带来的安全感——你不会不小心把数据写到了一个拼错名字的属性上。

坑二:继承时需要小心

如果父类有 __slots__,子类也应该定义自己的 __slots__,否则子类会自动加回 __dict__,优化就失效了:

class Base:
    __slots__ = ['x', 'y']

class Child(Base):
    # 没有定义 __slots__,会自动有 __dict__
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z  # 这个属性会存在 __dict__ 里

# 正确做法:
class ChildCorrect(Base):
    __slots__ = ['z']  # 只需声明子类新增的属性

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

坑三:不能配合 weakref 默认使用

如果你需要对对象用弱引用(weakref),需要在 __slots__ 里显式加上 '__weakref__':

class MyClass:
    __slots__ = ['data', '__weakref__']

什么时候该用

明确的判断标准:

  • 需要批量创建大量同类对象(成千上万以上)
  • 每个对象的属性集合是固定的、事先确定的
  • 对内存或访问速度敏感

典型场景:解析大量数据记录、游戏里的实体对象(粒子、怪物)、网络协议的包结构、机器学习特征行。

如果你只创建几个配置对象、几十个用户实例,加不加 __slots__ 意义不大,代码反而更死板。


写在最后

那次项目内存报警的事之后,我对 Python 对象的"重量"开始有了感知。

每个 Python 对象背后都挂着一本字典,这在灵活性上是天才设计,但在大量创建的场景下就变成了累赘。__slots__ 本质上是在说:我知道自己需要什么,不需要那本字典。

很多性能优化最终都是这个逻辑——把不确定性换成确定性,把灵活性换成效率。

学会什么时候该灵活,什么时候该约束自己,是写出好代码的关键。


觉得有收获的话点个赞吧,也欢迎转发给经常在服务器上跑批量数据处理的朋友。

有遇到过对象爆内存的经历吗?留言说说你是怎么解决的,看看大家都踩过哪些坑。

最后,别忘了关注「有为大青年」,我们下期见~

最新文章

随机文章