AIGC标识 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;数字均可复现)

posted @ 2026-11-02 09:59  小小张说故事  阅读(3)  评论(0)    收藏  举报