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

Python 列表(list) 的内存存储本质是 Python 中最容易被误解的高频数据结构之一。很多人以为它是链表(linked list),其实CPython 中的 list 是动态数组(dynamic array),但存储的不是值本身,而是指向对象的指针

下面从底层布局 → 为什么这么设计 → 与其他结构的存储差异 → 内存浪费原因 → 实际优化建议 完整拆解(基于 CPython 3.10~3.13+ 主流实现,2026 年视角)。

1. list 的真实内存布局(简化版)

PyListObject 结构体(C 层面)
├── PyObject_HEAD          # 引用计数 + 类型指针(~16 字节)
├── ob_size                # 当前元素个数(len(list))
├── allocated              # 已分配槽位数(≥ ob_size)
└── ob_item                # 指向 PyObject** 的指针数组(真正存数据的部分)
                                 ↓
                           [ ptr_to_obj1, ptr_to_obj2, ..., ptr_to_objN, NULL, NULL, ... ]
                           ↑               ↑
                     已使用槽位      预分配的空槽位(over-allocation)

关键点

  • list 不直接存值,存的是 8 字节指针(64 位系统)
  • 每个元素都是对 Python 对象的引用(int、str、自定义类等都是对象)
  • 即使存 1 亿个小整数,也不是存 1 亿×4 字节,而是 1 亿×8 字节的指针 + 对象本身的内存

2. 为什么 list 比 tuple 占更多内存?(最常见疑问)

结构可变?内存组成额外开销原因sizeof 示例(3 个 int)
list头部 + 指针数组 + over-allocation动态扩容预留空间 + 跟踪 allocated 大小~88–120 字节
tuple头部 + 刚好够用的指针数组无需预留、无需记录 allocated~64–80 字节

tuple 更省 的根本原因:

  • 固定长度 → 分配时精确知道需要多少槽位
  • 无需维护 allocated 字段(少 8 字节)
  • 无需 over-allocation(没有多余空槽)
  • 小 tuple 还会进入 intern/cache 机制(Python 内部缓存小 tuple)

3. over-allocation(预分配)策略(最核心设计)

为了让 append() 均摊 O(1),Python 故意多分配空间

增长公式(CPython 源码简化版):

new_allocated ≈ old_size * (9/8) + 6    # 大致 1.125 倍 + 小偏移
# 实际常见序列(字节对齐后):
0 → 4 → 8 → 16 → 25 → 35 → 46 → 58 → 72 → 88 → 106 → ...

效果

  • 连续 1000 次 append 只触发少数几次 realloc(拷贝)
  • 均摊时间复杂度 → O(1)
  • 但代价是空闲槽位浪费(allocated – len(list))

示例(实测常见情况):

lst = []
for i in range(100000):
    lst.append(i)
    # 某个时刻 len=50000,allocated 可能已到 65536+,浪费 ~15k 槽位

4. 常见存储差异对比表(同等元素个数)

数据结构每个元素开销(64位)over-allocation?适合场景内存效率排名
list~8 字节指针需要频繁 append/pop/insert中等
tuple~8 字节指针固定大小、作为 dict key、函数返回多值
array.array(‘i’)4 字节(int32)少量大量同类型整数/浮点很高
numpy.ndarray值本身(无指针)可控数值计算、海量同类型数据极高
collections.deque双向链表节点按块分配频繁两端 append/pop中低

一句话list 适合“不知道最终大小、需要修改”的场景已知大小或不变 → tuple / array / numpy 更省内存

5. 生产中常见的内存浪费场景 & 优化建议(2025–2026 实战)

场景浪费原因优化方式(优先级从高到低)预期节省
超大列表只读不改over-allocation + 指针开销转 tuple:tuple(big_list) 或直接用生成器/迭代器避免创建完整列表30–60%
百万级小整数/字符串列表每个元素 8 字节指针 + 对象开销array.array('i'/'l'/'f')numpy.array(..., dtype='int32')50–80%
频繁 append 在循环里多次小幅扩容(虽均摊 O(1),但有峰值)预先知道大致大小 → lst = [None] * estimated_size 再填充,或用 list.reserve()(3.13+实验)20–40%
list 作为 dict 的 key 或 set 元素list 不可 hash改用 tuple(天然可 hash)
嵌套多层小 list每层 list 都有头部 + 预分配展平为一维 + 用 numpy / struct,或自定义连续内存结构40–70%
临时列表只用来迭代一次完整物化占用内存用生成器表达式 (x for x in ...)map/filter 替代 [...]近 100%
列表中存大量相同小对象指针重复指向相同对象(但仍占 8B/个)无需优化(Python 小整数/短字符串已 intern),但考虑是否能用计数代替列表

代码示例对比(百万级整数)

import sys
import array
import numpy as np

N = 1_000_000

lst   = list(range(N))                  # ~ 指针开销大 + over-alloc
tup   = tuple(range(N))                 # 少 over-alloc
arr   = array.array('i', range(N))      # 紧凑,4 字节/int
nparr = np.array(range(N), dtype='i4')  # 最紧凑,通常连续内存

print(f"list   : {sys.getsizeof(lst)   + 8*N:>12,} 字节")   # ~8.4 MB + 对象
print(f"tuple  : {sys.getsizeof(tup)   + 8*N:>12,} 字节")   # ~8 MB
print(f"array  : {sys.getsizeof(arr)   + 4*N:>12,} 字节")   # ~4 MB
print(f"numpy  : {nparr.nbytes + nparr.itemsize*0:>12,} 字节")  # ~4 MB

结论排序(内存从省到费):numpy (紧凑值) > array > tuple > list

总结口诀(背下来就够用)

  • list 本质 = 指针数组 + 预分配动态数组
  • 指针开销 ≈ 8 字节/元素(64 位)
  • over-allocation ≈ 12.5% + 6 空位 → 为了 append 均摊 O(1)
  • 需要修改/未知大小 → list
  • 固定/只读/要 hash → tuple
  • 大量同类型数值 → array.array 或 numpy.ndarray(内存减半甚至更多)
  • 大列表先估大小 → 预分配 [None] * n 能减少拷贝次数

你当前代码里列表最大的痛点是哪种?

  • 百万级+数据内存爆炸?
  • append 性能卡?
  • 嵌套列表太深?
  • 只读但用了 list?

告诉我具体场景,我可以给你更针对性的结构替换 + 代码改造方案。

文章已创建 4915

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部