真实验证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))
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())会真实无限跑下去耗尽内存,这也是它作为无限迭代器最基本、也最容易被新手忽略的使用前提。
未排序数据 ["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只有一组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提供的这些工具真实能让"表示一个巨大的组合空间"这件事几乎不花任何内存代价。
每一步都是本文上面用真实的itertools跑出来的原始结果。
itertools.count / itertools.groupby / itertools.product