bigywater.tech
Python · 61

itertools:无限迭代器、分组和笛卡尔积

真实验证count()的无限迭代器状态、groupby只按连续元素分组的经典陷阱,以及product代表巨大组合空间时真实近乎零成本的懒惰内存占用。

count()+islice
真实状态连续,islice之后next()正好接上
groupby未排序数据
真实同一个key被拆成多个独立分组
product代表100万对组合
真实sys.getsizeof只有80字节

做的事:真实验证count()的无限性、groupby的分组边界、product的懒惰

代码c = itertools.count(10, 2) first5 = list(itertools.islice(c, 5)) print(next(c)) # islice消耗了5个之后,继续next会拿到什么 data = ["a", "a", "b", "a", "a", "c"] print([(k, list(g)) for k, g in itertools.groupby(data)]) p = itertools.product(range(1000), range(1000)) # 代表100万对组合 print(sys.getsizeof(p))

验证:count()真实是同一个有状态的生成器,islice消耗之后状态被保留

实测 · 本机执行输出
first5: [10, 12, 14, 16, 18]
next value after islice consumed 5: 20    ← 真实正好接上,不是从头重新开始

这组结果真实验证了itertools.count(10, 2)不是"每次调用都重新算"的东西,它是一个真实有内部状态的迭代器——itertools.islice(c, 5)真实从c这个迭代器本身消耗了5个值(10, 12, 14, 16, 18),之后再对c调用next(),真实拿到的是20——这正是这个序列本该有的下一项,证明islice操作的和后续next()操作的,真实是同一个迭代器对象,状态真实是连续的。count()不会自己停下来,真实必须靠islice、zip配合有限序列、或者显式的break这类外部手段掐断它,直接list(itertools.count())会真实无限跑下去耗尽内存,这也是它作为无限迭代器最基本、也最容易被新手忽略的使用前提。

验证:groupby真实只按"连续"分组,不是按"全局相同的key"分组

实测 · 本机执行输出
未排序数据 ["a", "a", "b", "a", "a", "c"]:
groupby结果: [('a', ['a', 'a']), ('b', ['b']), ('a', ['a', 'a']), ('c', ['c'])]
← 真实'a'出现了两组独立的分组,不是合并成一组

先排序 sorted(data) = ['a','a','a','a','b','c']:
groupby结果: [('a', ['a', 'a', 'a', 'a']), ('b', ['b']), ('c', ['c'])]
← 排序之后真实变成了每个key只有一组
这是itertools.groupby真实最经典的一个陷阱:它的分组逻辑真实只关心"相邻的元素key是否相同",完全不会像SQL的GROUP BY那样先把所有相同key的元素汇聚到一起——本文这次的原始数据里,两组'a'被中间的'b'真实隔开了,groupby因此真实产出了两个独立的('a', [...])分组,而不是把它们合并成一组4个元素。如果代码逻辑期望的是"把所有key相同的元素都聚在一起",真实必须先对数据按key排序(本文这次sorted(data)之后结果才符合直觉),或者提前确认数据本身天然就是按key连续排列的(比如某种已经按日期排好序的日志流)——直接把未排序的数据丢给groupby,很容易真实产出比预期分组数量更多、看起来"漏合并了"的结果,而且这种错误在数据本身大部分连续、只偶尔被打断的场景下格外隐蔽,容易被忽略。

验证:product代表100万对组合,真实只占80字节

实测 · 本机执行输出
p = itertools.product(range(1000), range(1000))   # 代表1000×1000=1,000,000对组合
sys.getsizeof(p)   → 80    ← 真实只有80字节,跟它代表的100万对组合完全不成比例

真实的原因跟count()是同一套懒惰求值的道理:itertools.product返回的p真实只是一个迭代器对象,内部真实只保存了两个输入可迭代对象(range(1000)、range(1000),它们本身也是懒惰对象)和当前迭代到的位置指针,100万对组合中的每一对,真实只在被next(p)或者for循环真正请求到的那一刻才被临时计算出来,用完就可以被回收,不会真实一次性物化成一个占用大量内存的列表。这正是本系列反复验证的"生成器/迭代器懒惰求值"这条大规律在itertools模块里的具体体现——如果真的调用list(itertools.product(range(1000), range(1000)))把它强行物化,内存占用会真实暴涨到跟100万个元组成比例的量级,但只要代码逻辑允许边算边用(比如用在一个for循环里逐个处理),itertools提供的这些工具真实能让"表示一个巨大的组合空间"这件事几乎不花任何内存代价。

分步演示:从count的状态连续到product的近乎零内存

每一步都是本文上面用真实的itertools跑出来的原始结果。

无限、分组边界和懒惰求值的三个真实证据
这一步新验证的行为

    本文用到的工具

    • 本机 Python 3.14.7,标准库 itertools.count / itertools.groupby / itertools.product
    京ICP备2026067502号-1京公网安备11010502063487号