Python 的 list 为什么能装下任何类型?拆开 PyObject* 数组,以及它让你多付了多少内存

先看一个几乎所有人都用过、但很少人想过的事:

python 复制代码
a = [1, "hello", 3.14, [1, 2], None, {"k": 1}]

整数、字符串、浮点、列表、None、字典------六种完全不同的东西,塞进同一个 list,Python 一点都不抗议。

Java 的 ArrayList<Integer> 装不进 String,C 的数组更不用说。为什么 Python 的 list 可以?

答案只有一句:list 里存的从来不是值,是指针。

拆开看:一块连续的 PyObject*

CPython 的 list 在内存里是两个东西:

  1. 一个 PyListObject 头(引用计数、类型指针、长度、已分配容量)
  2. 一块连续的数组,每个元素槽 8 个字节(64 位系统),里面放的是 PyObject* 指针

用 sys.getsizeof 就能看出这个 8 字节的槽:

python 复制代码
import sys
print(sys.getsizeof([]))      # 56
print(sys.getsizeof([1,2,3])) # 88
print((88 - 56) / 3)          # 10.67 ------ 不是 8?

等等,10.67 不是 8。因为 list 会预分配:创建一个 3 元素的 list 时,实际容量是 4,所以 56 + 4×8 = 88。

真正的槽宽验证要看扩容那一刻:

python 复制代码
lst = []
prev = sys.getsizeof(lst)
for i in range(1, 20):
    lst.append(i)
    cur = sys.getsizeof(lst)
    if cur != prev:
        print(len(lst), cur, "槽数:", (cur - 56) // 8)
        prev = cur

槽宽恒为 8 字节。这就对了------64 位指针。

最直接的一个证据:

python 复制代码
x = object()
lst = [x]
print(lst[0] is x)   # True

list 里那个位置和 x 是同一个对象,不是副本。所以 list 存的就是引用本身。

类型信息存在对象里,不在变量里

这就引出 Python 类型系统的核心设计:类型不属于变量,属于对象。

每个 Python 对象头上都有一个 PyObject_HEAD(引用计数 + 类型指针)。list 的槽里放的指针指过去,读一下类型指针就知道这是什么东西。

所以 list 根本不需要"元素类型一致"这个前提。它只要保证每个槽是一个合法的 PyObject* 就行。

代价也很直接:

python 复制代码
print(sys.getsizeof(1))     # 28
print(sys.getsizeof("a"))   # 42

一个 Python 的 int 对象 28 字节,其中真正存数值的部分只占几个字节------剩下全是对象头。

这笔内存账有多贵

做个极端点的实验,存 100 万个整数:

python 复制代码
import sys, array
N = 1_000_000
li = list(range(N))
ar = array.array('i', range(N))
print(sys.getsizeof(li))   # 8000056  (约 7.6 MB)
print(sys.getsizeof(ar))   # 4091948  (约 3.9 MB)

光看数组本身,list 是 array 的 2 倍:8 字节指针 vs 4 字节 int32。

但这只是"指针的钱",真正的开销在对象上------100 万个 int 对象,每个 28 字节:

sql 复制代码
list 数组本体   约  7.6 MB
100万个 int 对象 约 26.7 MB
---------------------------
合计            约 34.3 MB

array('i')      约  3.9 MB

约 9 倍。

这就是"能装任何类型"的价格:你为每一粒米都买了一个带包装的盒子。

那什么时候该换掉 list

先说结论:绝大多数业务代码不用管。一万、十万级别的数据,多出来的那点内存不值一提,list 的灵活性强太多。

但下面三种情况,换掉是实打实的收益:

1. 几百万级的数值数据 ------ 用 array 模块(标准库,零依赖),或者 numpy:

python 复制代码
from array import array
a = array('d', [1.0, 2.0, 3.0])   # 8 字节双精度,紧贴存储

2. 结构固定的记录 ------ __slots__ 或者 dataclass(slots=True),别让每个对象都背一个 dict:

python 复制代码
class Point:
    __slots__ = ('x', 'y')     # 去掉实例字典,省一大截
    def __init__(self, x, y): self.x, self.y = x, y

3. 只读的大序列 ------ tuple 没有预分配,也没有扩容逻辑,内存比同长度 list 略省,还不可变。

一句话总结

list 能装下任何类型,是因为它装的是指针数组,类型信息挂在对象自己头上,而不是容器上。

这个设计换来的是极致的灵活性:异构容器、任意嵌套、引用语义共享,全都免费。

代价是每个元素多付一个指针(8 字节)外加一个完整对象头。存 100 万个整数时,这个代价是 9 倍内存。

所以真正的分界线不是"该不该用 list",而是:你这批数据,是"东西"还是"数字"。 是东西就用 list,是数字就该想想 array 和 numpy 了。

相关推荐
chi11301 小时前
【无标题】
前端·javascript·python
编程见习者1 小时前
Deep Agents介绍
人工智能·python·langchain
梦幻精灵_cq1 小时前
《SGR修行之路》——二○二一五二〇,初识终端点红裳
python·生活
小蒜学长1 小时前
基于SpringBoot 框架的自媒体新闻系统的设计与实现(代码+数据库+LW)
java·spring boot·后端·mysql数据库·自媒体新闻管理系统
我要见SA姐11 小时前
Chrome 内存泄漏诊断指南:从现象定位到根因排查
ide·vscode·python·自动化·编辑器
应用市场2 小时前
用本机跑的开源音乐模型,给安卓小游戏做 20 秒无缝循环背景音乐
android·python·游戏开发·soundpool·音频处理
小lo想吃棒棒糖2 小时前
把 启元机器人Q1 的训练搬到昇腾 910B:从 MuJoCo 到 PPO 的一次完整迁移
人工智能·python·机器人·昇腾·具身智能
IT_陈寒2 小时前
JavaScript实战技巧总结:深挖数组遍历的性能陷阱与实战解法
前端·人工智能·后端
weixin_307779132 小时前
RAG 级联失效分析——面试中的错误归因艺术
人工智能·python·面试·职场和发展