Python 的 range 为什么能装下一亿个数还不卡?拆开惰性序列与懒计算
先做个实验:
import sys
print(sys.getsizeof(range(100))) # 48
print(sys.getsizeof(range(10**8))) # 48
range(1) 和 range(100000000) 占的内存一模一样,都是 48 字节。一亿个整数,每个对象按 28 字节算,真摆出来得 2.8GB——而 range 只用了 48 字节,连一张纸都占不满。
这不是魔法,是一种设计思想:惰性(lazy)——数据不用的时候,就不真的存在。
一、48 字节里装了什么
range 对象在 C 层面就是一个固定结构体,只存三个整数:start、stop、step。一亿个数并不存在于内存里,它们是一个"公式":
第 i 个元素 = start + i * step
你要第 5 个数,我现算给你;要第 9999 个,也是现算。存储被计算替代了——这就是惰性序列的本质。
对比一下代价(CPython 3.13 实测):
| 写法 | 内存 |
|---|---|
list(range(10**7)) |
80,000,056 字节 ≈ 80MB |
range(10**7) |
48 字节 |
一千万个数,差距是 166 万倍。
二、遍历不但不慢,反而更快
直觉上"现算"应该比"现成"慢,实测正好相反:
sum(list(range(10**7))) # 429ms
sum(range(10**7)) # 226ms,快 47%
原因藏在内存访问里:list 存的是 PyObject* 指针数组,每取一个元素要先解引用指针、再拿对象;range 每步直接做整数加法,连指针都不用碰。少一次解引用,一千万次循环就省出一半时间。
三、更离谱的:一亿个数里找元素只要 14 微秒
>>> 9999999 in range(10**8)
True # 耗时 14μs
>>> 9999999 in list(range(10**7))
True # 耗时 340ms
对 list 来说,in 是从头到尾逐个比对的 O(n) 扫描;而 range 知道自己的 start、stop、step,用一道算术题就能判断成员:
(x - start) 能被 step 整除,且 0 <= (x - start)/step < len
O(1) 对 O(n),这次是 24000 倍的差距。
四、range 是"懒",但它不是生成器
很多人把 range 和生成器混为一谈,其实它比生成器"实在"得多:
r = range(100)
len(r) # 100,可以随时问长度
r[50] # 50,可以随机访问
sum(r) # 遍历完了还能再来一遍
sum(r) # 重复迭代,结果不变
生成器是一次性的(耗尽即空),range 却可以反复遍历、切片、取 len。而且切片返回的还是 range:
range(100)[10:90:2] # range(10, 90, 2) —— 依然 48 字节
代价是它不可变:r[0] = 99 直接抛 TypeError。三个数字段的等差数列,本来就无处可改。
五、什么时候该用 range
- 只需要数数:
for i in range(n)永远优先于list(range(n)); - 大区间判断、等差遍历:range 的 O(1) 成员判断是隐藏福利;
- 需要反复遍历的等差序列:用 range,别转成 list,更别生成 list 之后又想省内存。
一句话总结:range 把"一亿个数"压缩成三个数加一个公式。 写 Python 时养成一个反射——先问自己"这份数据必须真的存在吗",很多时候内存和速度是白捡的。
(实测环境:CPython 3.13,Windows;数字均可复现)

浙公网安备 33010602011771号