Python 3 的 range 是什么?为什么 range(10**18) 不会撑爆内存?
简化版
Python 3 的 range 不是列表,而是一个「惰性的不可变序列对象」——它不会真的把所有数字存进内存,只保存 start、stop、step 三个值,需要某个数时按公式 start + i * step 现算,所以 range(10**18) 只占几十字节、不会撑爆内存。关键点:① 惰性(lazy)——range(n) 不生成 n 个数,占用的内存和 n 无关(永远是常数级);② 是「序列」不是「迭代器」——支持索引 r[i](O(1) 现算)、切片 r[2:5](返回新 range)、len(r)、in 判断(x in range(...) 是 O(1) 数学判断、不是逐个遍历),而且能重复遍历(迭代器只能遍历一次,range 可以反复用);③ 不可变、可比较(range(3) == range(0,3) 为 True,按值比较)。Python 2 的坑:Py2 的 range() 返回列表(真占内存)、xrange() 才是惰性;Py3 统一成惰性的 range(xrange 取消)。核心记忆:range 是惰性不可变序列(只存 start/stop/step、按需现算),内存与长度无关,支持索引/切片/O(1) 成员判断、可重复遍历;不是列表也不是一次性迭代器。
详细版
range 的特性:
| 特性 | 说明 |
|---|---|
| 惰性 | 只存 start/stop/step,内存 O(1)(与长度无关) |
| 是序列 | 支持索引 r[i]、切片 r[i:j]、len、reversed |
| 可重复遍历 | 不像迭代器只遍历一次,range 可反复迭代 |
| O(1) 成员判断 | x in range(...) 是数学计算,不是逐个查 |
| 不可变、可比较 | range(3) == range(0,3,1) 为 True |
# 惰性:巨大 range 也不占内存
r = range(10**18)
import sys
print(sys.getsizeof(r)) # 48(字节,常数级,和长度无关!)
print(sys.getsizeof(range(10))) # 48(一样大)
# 是序列:支持索引、切片、len
r = range(0, 100, 10) # 0,10,20,...,90
print(r[3]) # 30(O(1) 现算:0 + 3*10)
print(r[-1]) # 90
print(r[2:5]) # range(20, 50, 10)(切片返回新 range)
print(len(r)) # 10
# O(1) 成员判断(数学计算,不遍历)
print(50 in range(0, 10**9, 2)) # True(瞬间,不逐个查)
print(51 in range(0, 10**9, 2)) # False
# 可重复遍历(迭代器只能一次)
r = range(3)
print(list(r)) # [0, 1, 2]
print(list(r)) # [0, 1, 2](还能再遍历!)
# 可比较(按值,不看 start/step 表示形式)
print(range(0, 3) == range(3)) # True
print(range(0, 4, 2) == range(0, 5, 2)) # True(都是 0,2)
# 反向
print(list(reversed(range(3)))) # [2, 1, 0]
# 转列表才真正生成(占内存)
lst = list(range(10**6)) # 这才真的占内存
⚠️ 核心认知:Python 3 的
range是一个「假装成序列的公式」——它不存数据,只存 start/stop/step,任何操作(取第 i 个、判断 x 在不在、算长度)都是「用公式现场算」,所以内存永远是常数、range(10**18)和range(10)一样大。这解释了它的两个高频考点:①for i in range(n)高效——不会先造一个 n 元素的列表再遍历,而是边算边给,省内存又快;②x in range(...)是 O(1)——因为它用数学判断「x 是否等于某个start + i*step且在范围内」,不是像 list 的in那样逐个比较(O(n))。还要分清三个概念:range是惰性序列(可索引、可切片、可重复遍历);迭代器(如iter(range(3))、生成器)是一次性的(遍历完就空);list 是实打实存数据的。Python 2 里range()返回列表、xrange()才惰性,Python 3 把 range 统一成惰性、废弃了 xrange——这是 Py2→Py3 常见的认知调整。
完整版教学
一、range 是惰性序列,不是列表
先纠正最常见的误解:
误解:range(n) 生成一个包含 0~n-1 的列表
真相:range(n) 是一个"惰性序列对象",不存数据
range 对象内部只保存三个整数:
start(起始,默认 0)
stop(结束,不含)
step(步长,默认 1)
→ 就这三个值,不管 range 多大
生成第 i 个元素:按公式现算
range 的第 i 个元素 = start + i * step
→ 需要哪个才算哪个,不预先生成所有
内存证明:
sys.getsizeof(range(10)) → 48 字节
sys.getsizeof(range(10**18)) → 48 字节(一样!)
→ 内存和 range 长度无关(常数级)
对比 list:
list(range(10**6)) → 真的存 100 万个整数(占几十 MB)
range(10**6) → 只存 start/stop/step(48 字节)
为什么设计成惰性:
for i in range(n) 是最常见的循环
若每次都造一个 n 元素列表 → 浪费内存(尤其大 n)
惰性:边算边给,内存 O(1)、速度快
所以 range 是惰性序列(只存 start/stop/step、按公式现算),不是列表,内存 O(1)
误解:range(n) 生成一个包含 0~n-1 的列表。真相:range(n) 是一个「惰性序列对象」、不存数据。range 内部只保存三个整数:start(起始默认 0)、stop(结束不含)、step(步长默认 1)——就这三个值、不管 range 多大。生成第 i 个元素按公式现算:start + i * step(需要哪个才算哪个、不预先生成所有)。内存证明:sys.getsizeof(range(10)) 和 range(10**18) 都是 48 字节(内存和长度无关、常数级)。对比 list:list(range(10**6)) 真存 100 万整数(几十 MB)、range(10**6) 只存三个值(48 字节)。为什么设计成惰性:for i in range(n) 是最常见循环、若每次造 n 元素列表浪费内存、惰性边算边给内存 O(1)。理解「range 是惰性序列(只存 start/stop/step、第 i 个=start+i*step 现算)不是列表;内存 O(1)与长度无关(48 字节);list(range())才真占内存;惰性为省 for 循环内存」,就纠正了误解。
二、range 是序列——支持索引/切片/len
理解 range 作为「序列」的能力:
range 是"序列(sequence)",支持序列操作:
① 索引 r[i](O(1),现算):
r = range(0, 100, 10)
r[3] → 30(算 0 + 3*10)
r[-1] → 90(支持负索引)
→ 不遍历,直接公式算,O(1)
② 切片 r[i:j](返回新 range,不是 list):
r[2:5] → range(20, 50, 10)
r[::2] → range(0, 100, 20)(隔一个)
r[::-1] → range(90, -10, -10)(反向)
→ 切片仍是惰性 range(不生成数据)
③ len(r)(O(1)):
len(range(0, 100, 10)) → 10
→ 按 (stop-start)/step 算,不数
④ reversed(r):
reversed(range(3)) → range 反向迭代器 → 2,1,0
⑤ 迭代:
for i in range(n) # 边算边给
⑥ 其他:r.count(x)、r.index(x)、r.start/stop/step
不支持(因为不可变):
r[0] = 5 # ✗ 不能改(range 不可变)
r.append() # ✗ 无此方法
序列 vs 迭代器(关键):
range 是序列 → 可索引、可切片、可重复遍历、有 len
迭代器(生成器)→ 只能顺序遍历一次、无索引、无 len
所以 range 是序列:支持索引 r[i](O(1)现算)、切片(返回新 range)、len、reversed
range 是「序列(sequence)」,支持序列操作:① 索引 r[i](O(1)、现算)(range(0,100,10)[3] → 30、支持负索引、不遍历直接公式算);② 切片 r[i:j](返回新 range 不是 list)(r[2:5] → range(20,50,10)、r[::-1] 反向、仍是惰性 range);③ len(r)(O(1))(按 (stop-start)/step 算不数);④ reversed(r);⑤ 迭代(边算边给);⑥ 其他(count、index、start/stop/step)。不支持(因为不可变):r[0]=5、append。序列 vs 迭代器(关键):range 是序列(可索引、可切片、可重复遍历、有 len)、迭代器(生成器)只能顺序遍历一次(无索引、无 len)。理解「range 是序列:索引 ri、切片(返回新 range 仍惰性)、len(O(1))、reversed、count/index;不可变不能改;序列(可索引/切片/重复遍历)vs 迭代器(一次性)」,就掌握了 range 的序列能力。
三、O(1) 成员判断
理解 x in range(...) 为什么是 O(1):
x in range(...) 是 O(1)(数学判断,不遍历):
普通序列的 in(list/tuple):O(n)
x in [0,1,...,n-1] # 逐个比较,最坏 n 次
range 的 in:O(1)(对整数)
x in range(start, stop, step)
→ 用数学判断:
① start <= x < stop(在范围内)?
② (x - start) % step == 0(能被步长整除对齐)?
两个条件都满足 → x 在 range 里
→ 不遍历,直接算,O(1)
例子:
50 in range(0, 10**9, 2)
→ 0<=50<10**9 ✓ 且 (50-0)%2==0 ✓ → True(瞬间)
51 in range(0, 10**9, 2)
→ (51-0)%2 != 0 → False(瞬间)
→ 即使 range 有 5 亿个数,判断也是瞬间
注意:只对"整数"是 O(1)
非整数 x(如 float、其他类型)in range:
→ 退化成逐个比较(O(n)),但通常不会这么用
实用价值:
快速判断"某整数是否落在某等差序列里"
if age in range(18, 65): # 18<=age<65 的快速判断
if idx in range(len(arr)): # 索引合法性(虽然直接比较也行)
对比 list:
50 in list(range(10**9)) # 先造 5 亿的列表(撑爆内存) 再 O(n) 查
50 in range(10**9) # O(1),不占内存
→ range 完胜
所以 x in range 对整数是 O(1)(数学判断:在范围内+能被 step 整除),不遍历
x in range(...) 是 O(1)(数学判断、不遍历)。普通序列的 in(list/tuple)是 O(n)(逐个比较)。range 的 in 是 O(1)(对整数):用数学判断——① start <= x < stop(在范围内)② (x - start) % step == 0(能被步长整除对齐),两个条件都满足则 x 在 range 里(不遍历、直接算)。例子:50 in range(0, 10**9, 2) → 0<=50<10**9 且 50%2==0 → True(瞬间,即使 5 亿个数)。注意只对「整数」是 O(1)(非整数 in range 退化成逐个比较 O(n)、但通常不这么用)。实用价值:快速判断某整数是否落在某等差序列里(if age in range(18, 65))。对比 list:50 in list(range(10**9)) 先造 5 亿列表(撑爆内存)再 O(n) 查、50 in range(10**9) O(1) 不占内存。理解「x in range 对整数 O(1)(数学判断:start<=x<stop 且(x-start)%step==0),不遍历;list 的 in 是 O(n);实用快速判断整数在等差序列;非整数退化 O(n)」,就理解了 O(1) 成员判断。
四、序列 vs 迭代器 vs 列表
理解 range 和迭代器、列表的区别:
三个容易混的概念:
range(惰性序列):
✓ 惰性(不存数据、按需算)
✓ 可索引 r[i]、可切片、有 len
✓ 可重复遍历(迭代多少次都行)
✓ 不可变
→ "假装成序列的公式"
迭代器(iter、生成器):
✓ 惰性(按需产出)
✗ 不可索引、无 len
✗ 只能遍历一次(耗尽就空)
→ "一次性的数据流"
it = iter(range(3))
list(it) → [0,1,2]
list(it) → [](已耗尽!)
list(实体列表):
✗ 不惰性(真存所有数据、占内存)
✓ 可索引、可切片、有 len、可改
✓ 可重复遍历
→ "实打实的数据容器"
关键对比(可重复遍历 + 惰性):
重复遍历:range ✓、list ✓、迭代器 ✗
惰性省内存:range ✓、迭代器 ✓、list ✗
→ range 独占"惰性 + 可重复遍历 + 可索引"
range 可重复遍历的原理:
range 每次 for 都新建一个迭代器(不改变 range 本身)
→ 每次遍历都从头(因为 range 是"公式",随时能重算)
迭代器没有这个能力(它自己就是"进度",用完即止)
所以 range 惰性+可重复遍历+可索引;迭代器惰性但一次性;list 占内存但可重复
三个容易混的概念:range(惰性序列)——惰性(不存数据按需算)+ 可索引/切片/len + 可重复遍历 + 不可变(「假装成序列的公式」);迭代器(iter、生成器)——惰性但不可索引/无 len/只能遍历一次(耗尽就空,it = iter(range(3)); list(it) 第一次 [0,1,2]、第二次 []);list(实体列表)——不惰性(真存数据占内存)+ 可索引/切片/len/可改 + 可重复遍历。关键对比:重复遍历(range ✓、list ✓、迭代器 ✗)、惰性省内存(range ✓、迭代器 ✓、list ✗)——range 独占「惰性 + 可重复遍历 + 可索引」。range 可重复遍历的原理:每次 for 都新建一个迭代器(不改变 range 本身、每次从头、因为 range 是公式随时能重算),迭代器没有这能力(它自己就是进度、用完即止)。理解「range 惰性+可重复遍历+可索引(公式);迭代器惰性但一次性(耗尽即空);list 占内存但可重复;range 独占惰性+可重复+可索引;range 每次 for 新建迭代器所以能重复遍历」,就区分了三个概念。
五、Python 2 vs 3 与常见用法
理解版本差异和实用场景:
Python 2 vs Python 3(重要迁移点):
Python 2:
range(n) → 返回列表(真占内存!range(10**8) 会很慢/爆内存)
xrange(n) → 惰性(类似 Py3 的 range)
Python 3:
range(n) → 惰性(等价 Py2 的 xrange)
xrange → 取消(不存在了)
→ Py2 迁 Py3:xrange 改 range;依赖 range 返回列表的代码要 list(range())
常见用法:
① 循环 N 次:
for _ in range(n): ...
② 索引遍历(但优先 enumerate):
for i in range(len(seq)): ... # 尽量用 enumerate
③ 生成等差数列:
range(0, 100, 5) # 0,5,10,...,95
range(10, 0, -1) # 10,9,...,1(递减)
④ 快速造列表:
list(range(1, 11)) # [1,2,...,10]
⑤ 整数范围判断:
if x in range(18, 65): ... # O(1)
注意:
range 只支持整数(不能 range(0, 1, 0.1))
→ 浮点等差用 numpy.arange 或 [i*0.1 for i in range(10)]
省内存的意义:
sum(range(10**8)) # 惰性,不占内存,逐个累加
sum(list(range(10**8))) # 先造 1 亿列表(几百 MB)→ 浪费
所以 Py3 的 range 惰性(Py2 是列表、xrange 惰性);只支持整数、浮点用 numpy.arange
Python 2 vs Python 3(重要迁移点):Py2 的 range(n) 返回列表(真占内存、range(10**8) 爆内存)、xrange(n) 惰性;Py3 的 range(n) 惰性(等价 Py2 的 xrange)、xrange 取消——Py2 迁 Py3:xrange 改 range、依赖 range 返回列表的代码要 list(range())。常见用法:① 循环 N 次 for _ in range(n)、② 索引遍历(优先 enumerate)、③ 生成等差数列 range(0,100,5)、range(10,0,-1) 递减、④ 快速造列表 list(range(1,11))、⑤ 整数范围判断 if x in range(18,65)(O(1))。注意:range 只支持整数(不能 range(0,1,0.1))、浮点等差用 numpy.arange 或 [i*0.1 for i in range(10)]。省内存的意义:sum(range(10**8)) 惰性不占内存、sum(list(range(10**8))) 先造 1 亿列表浪费。理解「Py3 range 惰性(Py2 是列表、xrange 惰性,迁移要改);用法循环 N 次/等差数列/递减 range(10,0,-1)/快速造列表;只支持整数、浮点用 numpy.arange;sum(range())省内存」,就掌握了版本差异与用法。
六、总结与实践
总结 range 的特性:
核心特性:
range 是"惰性、不可变序列"
只存 start/stop/step,第 i 个元素 = start + i*step(现算)
内存 O(1)(与长度无关)
能力:
可索引 r[i](O(1))、可切片(返回新 range)、len(O(1))
可重复遍历(不像迭代器一次性)
x in range 对整数 O(1)(数学判断)
不可变、可比较、可 reversed
三者区别:
range:惰性 + 可索引 + 可重复遍历(公式)
迭代器:惰性 + 一次性(数据流)
list:占内存 + 可索引 + 可重复(实体)
实用:
循环 for i in range(n)
等差数列 range(start, stop, step)
整数范围判断 x in range(a, b)(O(1))
巨大范围也不占内存(range(10**18))
坑/注意:
Py2 range 返回列表、xrange 惰性(Py3 统一惰性)
只支持整数(浮点用 numpy.arange)
需要真列表时 list(range(...))
核心总结:
range 惰性不可变序列(只存 3 个值、按需算)
内存 O(1)、可索引/切片/重复遍历、in 是 O(1)
不是列表也不是一次性迭代器
所以 range 惰性序列(存 3 值现算),内存 O(1),可索引/切片/重复遍历,in 是 O(1)
核心特性:range 是「惰性、不可变序列」(只存 start/stop/step、第 i 个 = start + i*step 现算、内存 O(1) 与长度无关)。能力:可索引 r[i](O(1))、可切片(返回新 range)、len(O(1))、可重复遍历、x in range 对整数 O(1)、不可变可比较可 reversed。三者区别:range(惰性+可索引+可重复遍历)、迭代器(惰性+一次性)、list(占内存+可索引+可重复)。实用:循环、等差数列、整数范围判断、巨大范围不占内存。坑/注意:Py2 range 返回列表(Py3 统一惰性)、只支持整数(浮点用 numpy.arange)、需要真列表用 list(range())。理解「range 惰性不可变序列(存 3 值按需算);内存 O(1)、可索引/切片/重复遍历、in 是 O(1);不是列表也不是一次性迭代器;Py2 是列表、只支持整数」,就掌握了总结与实践。
记忆钩子:「Python 3 的 range 不是列表而是『惰性的不可变序列对象』——它只保存 start/stop/step 三个整数,不真的存所有数字,第 i 个元素按公式 start+i*step 现算,所以★range(1018)只占 48 字节(和 range(10)一样大)、内存 O(1)与长度无关,不会撑爆内存;range 是『序列』不是『迭代器』:①支持索引 ri、切片 r[2:5](返回新 range 仍惰性)、len(O(1))、reversed②★能重复遍历(迭代器只能遍历一次耗尽即空,range 每次 for 都新建迭代器、可反复用)③★x in range 对整数是 O(1)(用数学判断:start<=x<stop 且(x-start)%step==0,不是像 list 那样逐个比较 O(n))④不可变、可比较(range(3)==range(0,3)为 True);三者区分:range 惰性+可索引+可重复遍历(公式)、迭代器惰性但一次性(数据流)、list 占内存但可索引可重复(实体);★Py2 的坑:Py2 的 range()返回列表(真占内存)、xrange()才惰性,Py3 统一成惰性 range、废弃 xrange;range 只支持整数(浮点等差用 numpy.arange),需要真列表用 list(range())」**。
七、常见误区与追问
- 误区:range(n) 会生成一个包含 n 个数字的列表。 不会——Python 3 的 range 是惰性序列对象,内部只保存 start、stop、step 三个整数,不存储任何数字,需要第 i 个元素时才按公式
start + i * step现算;所以range(10**18)只占约 48 字节(和range(10)一样大)、内存和长度无关;只有list(range(n))才会真正生成 n 个数字、占用大量内存。 - 误区:range 和生成器一样只能遍历一次。 不是——range 是「序列」不是「一次性迭代器」,可以反复遍历任意多次(每次 for 循环都会基于 range 新建一个迭代器、从头开始,因为 range 本质是「公式」、随时能重算);生成器和
iter(range(3))这样的迭代器才是一次性的(遍历完就耗尽、再遍历得到空);range 还支持索引、切片、len,这些生成器都不支持。 - 误区:x in range(…) 和 x in list 一样是 O(n) 遍历。 对整数 x 是 O(1)——range 的成员判断用数学计算:检查
start <= x < stop且(x - start) % step == 0,两个条件都满足就在范围内,不需要逐个遍历;所以50 in range(0, 10**9, 2)瞬间返回、即使范围有 5 亿个数;而x in list(...)是逐个比较、O(n);注意如果 x 不是整数(如 float),range 的 in 会退化成逐个比较。 - 误区:range 支持浮点步长,比如 range(0, 1, 0.1)。 不支持——range 只接受整数参数(start、stop、step 都必须是整数),
range(0, 1, 0.1)会抛 TypeError;需要浮点等差数列用numpy.arange(0, 1, 0.1)、或用列表推导式[i * 0.1 for i in range(10)]、或numpy.linspace(指定端点和个数)。 - 追问:为什么 range(1018) 不会撑爆内存,而 list(range(1018)) 会? 因为 range 是惰性的——
range(10**18)只是创建了一个对象、内部保存 start=0、stop=10**18、step=1 这三个整数(外加对象头),总共约 48 字节,不管范围多大都是这个常数级大小,需要某个数时才用公式start + i*step现算;而list(range(10**18))会强制「物化」——真的去生成 10^18 个整数对象并存进列表,这需要天文数字的内存(每个 int 对象几十字节 × 10^18),根本不可能分配、会直接内存溢出;所以能用惰性 range 就别转成 list,比如sum(range(10**8))是逐个累加、几乎不占内存,sum(list(range(10**8)))则要先造一个几百 MB 的列表。 - 追问:range、迭代器、list 三者有什么区别,各适合什么场景? range 是「惰性的不可变序列」:不存数据(内存 O(1))、但支持索引/切片/len、可重复遍历、成员判断对整数 O(1)——适合「表示一个整数范围、做循环计数、范围判断」;迭代器(含生成器)是「惰性的一次性数据流」:按需产出、省内存、但不能索引/无 len/只能遍历一次——适合「处理大数据流、一次性消费、惰性计算管道」;list 是「实体容器」:真存所有数据(占内存)、可索引/切片/修改、可重复遍历——适合「需要随机访问、修改、多次使用具体数据」的场景;简言之:范围/计数用 range,流式处理用生成器,需要存取具体数据用 list。
- 追问:Python 2 和 Python 3 的 range 有什么不同,迁移时要注意什么? Python 2 里
range(n)返回一个真正的列表(会立即生成所有元素、占内存,range(10**8)会很慢甚至内存溢出),而xrange(n)才是惰性的(类似 Python 3 的 range);Python 3 把 range 统一改成了惰性版本(等价于 Py2 的 xrange)、并取消了 xrange(不再存在);迁移注意:① Py2 代码里的xrange要改成range;② 如果 Py2 代码依赖「range 返回列表」的行为(比如range(3) + [4]拼接、或对结果做.append、索引赋值),在 Py3 里要显式list(range(3));③ 好处是迁移后大范围循环自动变省内存了(不用再手动用 xrange)。
八、加强记忆
Python 3 的 range 不是列表而是「惰性的不可变序列对象」——它只保存 start/stop/step 三个整数、不真的存所有数字,第 i 个元素按公式 start + i * step 现算,所以 range(10**18) 只占 48 字节(和 range(10) 一样大)、内存 O(1) 与长度无关、不会撑爆内存。range 是「序列」不是「迭代器」:① 支持索引 r[i](O(1) 现算)、切片 r[2:5](返回新 range、仍惰性)、len(O(1))、reversed;② 能重复遍历(迭代器只能遍历一次、耗尽即空,range 每次 for 都新建迭代器、可反复用);③ x in range 对整数是 O(1)(用数学判断 start <= x < stop 且 (x-start) % step == 0,不是像 list 那样逐个比较 O(n));④ 不可变、可比较(range(3) == range(0,3) 为 True)。三者区分:range(惰性+可索引+可重复遍历、公式)、迭代器(惰性但一次性、数据流)、list(占内存但可索引可重复、实体)。Py2 的坑:Py2 的 range() 返回列表(真占内存)、xrange() 才惰性,Py3 统一成惰性 range、废弃 xrange。range 只支持整数(浮点等差用 numpy.arange),需要真列表用 list(range())。一句话「range 是惰性不可变序列(只存 start/stop/step、按公式现算),内存 O(1)、range(1018)才 48 字节;可索引/切片/len/可重复遍历、x in range 对整数 O(1);不是列表也不是一次性迭代器;Py2 的 range 返回列表、xrange 才惰性**」。