先看一个几乎所有人都用过、但很少人想过的事:
a = [1, "hello", 3.14, [1, 2], None, {
"k": 1}]
整数、字符串、浮点、列表、None、字典——六种完全不同的东西,塞进同一个 list,Python 一点都不抗议。
Java 的 ArrayList<Integer> 装不进 String,C 的数组更不用说。为什么 Python 的 list 可以?
答案只有一句:list 里存的从来不是值,是指针。
拆开看:一块连续的 PyObject*
CPython 的 list 在内存里是两个东西:
- 一个
PyListObject头(引用计数、类型指针、长度、已分配容量) - 一块连续的数组,每个元素槽 8 个字节(64 位系统),里面放的是
PyObject*指针
用 sys.getsizeof 就能看出这个 8 字节的槽:
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。
真正的槽宽验证要看扩容那一刻:
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
# 1 88 槽数: 4
# 5 120 槽数: 8
# 9 184 槽数: 16
槽宽恒为 8 字节。这就对了——64 位指针。
最直接的一个证据:
x = object()
lst = [x]
print(lst[0] is x) # True
list 里那个位置和 x 是同一个对象,不是副本。所以 list 存的就是引用本身。
类型信息存在对象里,不在变量里
这就引出 Python 类型系统的核心设计:类型不属于变量,属于对象。
每个 Python 对象头上都有一个 PyObject_HEAD(引用计数 + 类型指针)。list 的槽里放的指针指过去,读一下类型指针就知道这是什么东西。
所以 list 根本不需要"元素类型一致"这个前提。它只要保证每个槽是一个合法的 PyObject* 就行。
代价也很直接:
print(sys.getsizeof(1)) # 28
print(sys.getsizeof("a")) # 42
一个 Python 的 int 对象 28 字节,其中真正存数值的部分只占几个字节——剩下全是对象头。
这笔内存账有多贵
做个极端点的实验,存 100 万个整数:
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 字节:
list 数组本体 约 7.6 MB
100万个 int 对象 约 26.7 MB
---------------------------
合计 约 34.3 MB
array('i') 约 3.9 MB
约 9 倍。
这就是"能装任何类型"的价格:你为每一粒米都买了一个带包装的盒子。
那什么时候该换掉 list
先说结论:绝大多数业务代码不用管。一万、十万级别的数据,多出来的那点内存不值一提,list 的灵活性强太多。
但下面三种情况,换掉是实打实的收益:
1. 几百万级的数值数据 —— 用 array 模块(标准库,零依赖),或者 numpy:
from array import array
a = array('d', [1.0, 2.0, 3.0]) # 8 字节双精度,紧贴存储
2. 结构固定的记录 —— __slots__ 或者 dataclass(slots=True),别让每个对象都背一个 dict:
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 了。