python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python列表内存存储本质

Python列表内存存储本质之存储差异原因与优化建议总结

作者:dudly

在Python中,列表(list)作为最常用的数据结构之一,其内存存储机制直接影响程序性能,下面这篇文章主要介绍了Python列表内存存储本质之存储差异原因与优化建议的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

前言

在 Python 中处理大量字符串时,你可能会遇到意想不到的内存占用问题。比如需要存储一百万个短字符串或数字,按每个字符串平均 10 字节、每个 64 位整数 8 个字节计算,理论上只需约 8 到 10MB 内存,但实际用 Python 列表存储时,内存使用可能会到几十MB。这背后的原因是什么?又该如何优化?

1. 问题引入:列表存储的内存 “膨胀”

先看一段简单的代码,用普通列表存储一百万个短字符串、相同的短字符串、整数、相同的整数:

str_list = [f"item_{i}" for i in range(1000000)]
same_item_str_list = [f"item" for i in range(1000000)]
num_list = [i for i in range(1000000)]
same_item_num_list = [0 for i in range(1000000)]

直觉上,每个字符串 “item_xxx” 大约 8-10 字节,每个整数 8 个字节,一百万条数据应该在 8 到 10MB 左右。但实际内存使用如何呢,我们用pympler来精确测量。

先安装 pympler:

uv add pympler

修改代码,增加测量内存占用情况的打印:

from pympler import asizeof

str_list = [f"item_{i}" for i in range(1000000)]
same_item_str_list = [f"item" for i in range(1000000)]
num_list = [i for i in range(1000000)]
same_item_num_list = [0 for i in range(1000000)]

print(f"str_list列表内存: {asizeof.asizeof(str_list) / 1024 / 1024:.2f} MB")
print(f"same_item_str_list列表内存: {asizeof.asizeof(same_item_str_list) / 1024 / 1024:.2f} MB")
print(f"num_list列表内存: {asizeof.asizeof(num_list) / 1024 / 1024:.2f} MB")
print(f"same_item_num_list列表内存: {asizeof.asizeof(same_item_num_list) / 1024 / 1024:.2f} MB")

再次运行,得到的内存报告大致如下(具体数值因环境略有差异):

str_list列表内存: 61.46 MB
same_item_str_list列表内存: 8.06 MB
num_list列表内存: 38.57 MB
same_item_num_list列表内存: 8.06 MB

可以看到,四个列表的内存占用差异巨大:存储不同字符串的str_list占用 61.46MB,存储不同整数的num_list占用 38.57MB,而存储相同字符串和相同数字的列表都只占用约 8MB 内存。为什么同样是存储一百万条数据,内存占用会相差这么大呢?为什么和我们的根据理论猜测的占用大小不一样呢?这需要先从数据的理论存储与实际存储差异说起。

2. 理论存储与实际存储的差异

我们常说 “每个 64 位整数占用 8 字节”“每个字符占用 1 字节”,这是硬件层面的理论存储需求,但在 Python 中,由于对象模型的设计,实际存储开销会远高于理论值。

2.1 64位整数的存储差异

2.2 短字符串的存储差异

3. 列表的内存存储本质

了解了整数和字符串的理论存储和实际存储差异,我们就可以开始学习列表的内存存储了。Python 列表本质上是指针数组,它存储的不是元素本身,而是元素对象在内存中的地址(指针)。在 64 位系统中,每个指针固定占用 8 字节,因此:

3.1 相同元素列表内存少的核心原因:对象复用

当列表中的元素完全相同时(如same_item_str_list全是 “item”,same_item_num_list全是 0),Python 会复用同一个对象,避免重复创建,从而大幅减少内存开销。

3.1.1 小整数的缓存复用机制

Python 对小整数(通常是 -5 到 256 之间) 采用预创建和缓存机制:这些整数在 Python 启动时就被提前创建,并存入全局缓存池,后续使用时直接复用,不会重复分配内存。

3.1.2 字符串的驻留(Intern)机制

Python 会对短字符串、标识符类字符串进行 “驻留”(Intern)处理:相同的字符串会被存储在全局字符串池中,后续使用时直接复用,不会重复创建新对象。

3.2 不同元素列表内存高的原因:对象重复创建

当列表中的元素不同时(如str_listnum_list),每个元素都是独立的新对象,需要为每个元素分配单独的内存,导致总内存剧增。

3.2.1 不同整数的内存开销

num_list = [i for i in range(1000000)]中,元素是 0 到 999999:

3.2.2 不同字符串的内存开销

str_list = [f"item_{i}" for i in range(1000000)]中,每个元素是不同的字符串(“item_0” 到 “item_999999”):

4. 内存占用对比分析

列表类型指针数组内存(固定)元素对象内存(变量)总内存内存差异原因
same_item_num_list8MB28 字节(1 个 0 对象)8.06MB小整数缓存复用,元素内存可忽略
num_list8MB≈27MB(约 99 万个大整数)38.57MB大整数无缓存,每个都是新对象
same_item_str_list8MB50 字节(1 个 “item” 对象)8.06MB字符串驻留复用,元素内存可忽略
str_list8MB≈48MB(100 万个不同字符串)61.46MB动态字符串无驻留,每个都是新对象

5. 优化建议:利用对象复用减少内存开销

了解了 Python 的对象复用机制后,我们可以采取以下策略优化列表内存占用:

  1. 复用小整数和短字符串:在需要存储大量重复元素的场景中,尽量使用小整数(-5 到 256)和可驻留的短字符串,避免动态生成不同的元素。
  2. 使用数据结构优化重复元素存储:对于包含大量重复元素的列表,可使用array模块或 Pandas 的category类型,这些结构会自动复用重复元素,减少内存开销。
  3. 避免无意义的对象创建:在循环中避免重复创建相同的对象,例如将[f"abcdefghijklmnopqrstuvwxyz" for i in range(1000000)]改为item = "abcdefghijklmnopqrstuvwxyz"; [item for i in range(1000000)],确保元素对象只创建一次。
  4. 针对大整数和长字符串的优化:对于大量大整数,可考虑使用 NumPy 数组存储;对于大量字符串,可使用 Pandas 的StringDtypecategory类型,利用其内置的重复元素压缩机制。

6. 总结

Python 列表的内存占用差异主要来自元素对象的复用情况:相同元素的列表通过小整数缓存和字符串驻留机制复用对象,内存开销主要来自指针数组;而不同元素的列表需要为每个元素创建独立对象,每个对象的元数据开销累积导致内存飙升。

在实际开发中,当需要存储大量数据时,应充分利用 Python 的对象复用机制,选择合适的数据结构,避免无意义的对象重复创建。通过合理设计数据存储方式,既能减少内存占用,也能提高程序运行效率。

您可能感兴趣的文章:
阅读全文