Python 的 list.append 为什么是 O(1)?拆开 CPython 的扩容公式,以及 127.8 毫秒背后的 86 次搬内存
教科书上说 list.append() 是均摊 O(1)。
但直觉上这很反常:list 底层是一块连续内存,加一个元素就得重新分配一块更大的、把旧的拷过去。拷一次是 O(n),拷 n 次不就是 O(n²)?
教科书没错,直觉也没完全错——关键是它不是每次都拷。
先看真实的扩容行为
用 sys.getsizeof 盯住每一次内存变化:
import sys
lst = []
prev = sys.getsizeof(lst)
for i in range(1, 200):
lst.append(i)
cur = sys.getsizeof(lst)
if cur != prev:
print(len(lst), cur, "容量:", (cur - 56) // 8)
prev = cur
输出(CPython 3.13.14):
len=1 88 容量 4
len=5 120 容量 8
len=9 184 容量 16
len=17 248 容量 24
len=25 312 容量 32
len=33 376 容量 40
len=41 472 容量 52
len=53 568 容量 64
len=65 664 容量 76
len=77 792 容量 92
len=93 920 容量 108
len=109 1080 容量 128
注意看:它一次多申请一大截。第一次就给了 4 个位置(你只用了 1 个),满了不是加 1 个,而是直接跳到 8。
容量到底怎么算出来的
CPython 源码 Objects/listobject.c 里 list_resize() 的那行是这个:
new_allocated = ((size_t)newsize + (newsize >> 3) + 6) & ~(size_t)3;
翻译成人话:
newsize—— 这次需要的最小容量newsize >> 3—— 额外多给 12.5%(除以 8)+ 6—— 保底多给 6 个& ~3—— 向下取整到 4 的倍数
逐个验证上面的实测值,全中:
| newsize | 计算 | & ~3 之后 | 实测容量 |
|---|---|---|---|
| 5 | 5+0+6 = 11 | 8 | 8 ✅ |
| 9 | 9+1+6 = 16 | 16 | 16 ✅ |
| 17 | 17+2+6 = 25 | 24 | 24 ✅ |
| 25 | 25+3+6 = 34 | 32 | 32 ✅ |
| 41 | 41+5+6 = 52 | 52 | 52 ✅ |
| 65 | 65+8+6 = 79 | 76 | 76 ✅ |
| 77 | 77+9+6 = 92 | 92 | 92 ✅ |
所以增长比例不是固定的 2 倍(Python 早期版本是),而是前期接近 2 倍、后期收敛到约 1.125 倍。数据量大时更省内存,这是 3.x 之后的改进。
均摊 O(1) 是怎么成立的
既然每次扩容大约多给 12.5%,那么"距离下次扩容"的长度也在按比例增长。数学上的结果就是:n 次 append 的总拷贝量是 O(n),均摊下来每次 O(1)。
实测数一下扩容次数:
def grow_count(n):
lst, last, c = [], sys.getsizeof([]), 0
for i in range(n):
lst.append(i)
s = sys.getsizeof(lst)
if s != last:
c += 1; last = s
return c
for n in (10, 100, 1000, 10000, 100000, 1000000):
print(n, grow_count(n))
结果:
n=10 → 3 次
n=100 → 11 次
n=1000 → 28 次
n=10000 → 47 次
n=100000 → 66 次
n=1000000 → 86 次
一百万次 append,只重新分配了 86 次内存。 这就是均摊 O(1) 的实证——拷贝次数不是线性增长的,是对数级的。
实战:预分配还有意义吗
很多优化文章会说"先 [0]*n 再按索引赋值更快"。实测一下:
import time
# A: append 一百万次
st = time.perf_counter()
b = []
for i in range(1_000_000): b.append(i)
t1 = (time.perf_counter() - st) * 1000
# B: 先建好再赋值
st = time.perf_counter()
c = [0] * 1_000_000
for i in range(1_000_000): c[i] = i
t2 = (time.perf_counter() - st) * 1000
结果(3.13.14,本机):
append: 127.8 ms
预分配赋值: 123.2 ms
差距: 3.8%
只快 3.8%。 因为 over-allocation 已经把重新分配的开销摊得几乎看不见了。
所以结论很明确:为了性能去做预分配,基本是白费劲,还会让代码变丑。真正该用的是列表推导式,它连方法调用都省了。
两个反直觉的补充
1. list 会缩容。 很多人以为它只增不减,其实不是:
d = list(range(1000))
print(sys.getsizeof(d)) # 8056
for _ in range(900): d.pop()
print(sys.getsizeof(d)) # 1528 ← 缩了
list_resize 里有一条:如果新长度小于已分配容量的一半,就重新分配一块小的。
2. clear() 会把容量清零。
d.clear()
print(sys.getsizeof(d)) # 56 ← 回到初始状态
所以长期持有又反复清空的大 list,不用担心内存一直挂着。
一句话总结
append 之所以是 O(1),不是因为它不搬内存,而是因为它攒够了才搬一次:新容量 = (需要的 + 需要的/8 + 6) 再向下取整到 4 的倍数,超额配置约 12.5%。
一百万次 append 只触发 86 次重新分配,实测与预分配写法只差 3.8%——所以别再为了"性能"手动预分配了,把精力花在算法上更划算。

浙公网安备 33010602011771号