@toc
环境:Python 3.8+(
batched需 3.12+,pairwise需 3.10+) | 适用人群:Python 初中级开发者、数据处理工程师
引言:为什么你需要掌握 itertools?
你是不是经常遇到这些场景:
- 处理几百万行日志文件,一个
list()就把内存撑爆了 - 写一堆嵌套循环做排列组合,代码又长又容易出错
- 想合并多个数据源,却总在
list_a + list_b时创建不必要的中间列表 - 跑测试时需要批量生成参数组合,手写循环写到怀疑人生
itertools 正是 Python 标准库为这些痛点准备的答案。它提供了一套高效、内存友好的迭代器构建块,核心优势只有一条:只在需要时才产生下一个值。
读完本文你将掌握:
- 迭代器与可迭代对象的本质区别,以及为什么「一次性消费」是特性而非 bug
- itertools 三大类函数(无限迭代器 / 有限迭代器 / 组合生成器)的 20+ 核心用法
- 用 itertools 替代列表操作的性能收益------不只是「快一点」,而是 O(1) vs O(n) 的内存差距
- 可直接复用的实战代码,以及我在生产环境中踩过的坑
💡 我的踩坑实录 :第一次用
chain()时,我以为它返回的是列表,在list(res_ch)之后又用next()遍历,直接抛了StopIteration。排查半天才意识到:迭代器一旦耗尽就不能再次使用,这和列表可以反复遍历完全不同。从那以后,我每次用 itertools 都会问自己一句话:「这个东西还能迭代第二次吗?」
1. 迭代器基础:为什么「一次性」是好事?
在理解 itertools 之前,必须先搞清楚一个概念上的区分。
1.1 可迭代对象 vs 迭代器
在 Python 中,可迭代对象 (Iterable)是任何可以放进 for 循环的东西------列表、元组、字典、文件对象、生成器。它们实现了 __iter__() 方法。
迭代器 (Iterator)则是真正「产生值」的那个对象------它实现了 __next__() 方法,每次调用返回下一个元素,直到抛出 StopIteration。
python
# 可迭代对象 vs 迭代器
my_list = [1, 2, 3] # 可迭代对象,但不是迭代器
my_iter = iter(my_list) # 通过 iter() 获取迭代器
print(next(my_iter)) # 1
print(next(my_iter)) # 2
print(next(my_iter)) # 3
# print(next(my_iter)) # StopIteration ------ 迭代器已耗尽
💡 关键认知:列表可以遍历无数次,但迭代器只能消费一次。这不是 bug,而是内存效率的来源------迭代器不存储已产生的值,所以 O(1) 内存就能处理无限数据流。
1.2 itertools 的核心优势
itertools 的所有函数返回的都是迭代器。这意味着三个关键特性:
| 特性 | 含义 | 实际影响 |
|---|---|---|
| 惰性计算 | 只在 next() 调用时才生成下一个值 |
不会预先生成所有结果 |
| O(1) 内存 | 迭代器对象大小恒定(通常 56 字节) | 处理 1000 万行数据和 10 行数据内存占用相同 |
| 无限序列 | 可以表达无限长的数据流 | count() 永远不会停止,需要配合 islice 截断 |
2. 无限迭代器:永不停止的数据流
无限迭代器是 itertools 最独特的特性之一。它们可以无限产生元素 ------别担心,只要不用 list() 包裹它们,内存就不会爆炸。
2.1 count():数字的无限递增
count(start=0, step=1) 从 start 开始,以 step 为步长无限递增。它的底层是一个生成器,每次 next() 调用才计算下一个值。
python
import itertools
# 从 100 开始,每次增加 50
count_iter = itertools.count(start=100, step=50)
print(next(count_iter)) # 100
print(next(count_iter)) # 150
print(next(count_iter)) # 200
# 可以一直 next() 下去,永远不会停止
实用场景:
- 自动生成递增 ID:
next(itertools.count(1000)) - 配合
enumerate替代方案:zip(itertools.count(), data) - 模拟无限数据流用于测试
⚠️ 注意:
count()只能对数字递增。如果需要对任意可迭代对象做无限循环,请用cycle()。
2.2 cycle():无限循环任意可迭代对象
cycle(iterable) 会不断重复一个可迭代对象。到达末尾后自动回到开头------你永远不会看到 StopIteration。
python
import itertools
times = list(range(0, 12))
cycle_iter = itertools.cycle(times)
# 取前 15 个值------超过 12 后会从头循环
for _ in range(15):
print(next(cycle_iter), end=" ")
# 输出:0 1 2 3 4 5 6 7 8 9 10 11 0 1 2
💡 注意区分:
times是列表(可迭代对象但不是迭代器),所以对它next(times)会报TypeError: 'list' object is not an iterator。而cycle(times)返回的是一个迭代器,可以无限next()。这是前面 1.1 节那个「可迭代对象 vs 迭代器」区分在实战中的直接体现。
实用场景:
- 轮询调度(Round-Robin):
cycle(["server1", "server2", "server3"]) - 周期性任务标记:
cycle(["morning", "afternoon", "evening"]) - 生成重复的测试数据模式
⚠️ 注意:
cycle()会在内部缓存整个输入序列 (因为需要反复回绕)。如果输入是百万级列表,cycle()会一次性把它们全部缓存在内存中。这与 itertools「O(1) 内存」的一般承诺不同------对于大序列,考虑用count() % n替代。
2.3 repeat():重复同一个值
repeat(obj, times=None) 重复生成同一个对象。times 参数指定次数,不传则无限重复。
python
import itertools
# 重复 5 次
limited = itertools.repeat("任务完成", times=5)
print(list(limited))
# ['任务完成', '任务完成', '任务完成', '任务完成', '任务完成']
# 无限重复(需要配合 islice 截断)
infinite = itertools.repeat("心跳信号")
💡
repeat()的一个高阶用法是配合map():map(pow, range(10), repeat(2))计算 0² 到 9²,比[x**2 for x in range(10)]更具函数式风格------在需要将固定参数传入map的多参数函数时非常有用。
现在来看 repeat() 的一个重要细节------times 参数决定了迭代上限:
python
import itertools
limited = itertools.repeat("数据", times=5)
# 遍历 6 次------第 6 次会报错
for _ in range(6):
print(next(limited), end=" ")
# StopIteration ------ 第 6 次调用时迭代器已耗尽
3. 有限迭代器:数据流管道的基础组件
有限迭代器处理「有尽头」的数据。它们不会无限生成值,但同样享受惰性求值带来的内存优势。
3.1 chain():把多个可迭代对象串成一条流水线
chain(*iterables) 按顺序依次消费每个可迭代对象,返回一个连续的迭代器。
python
import itertools
source_1 = range(10)
source_2 = (x + 1 for x in source_1) # 生成器表达式
source_3 = ("a", "b", "c")
pipeline = itertools.chain(source_1, source_2, source_3)
print(list(pipeline))
# [0,1,2,3,4,5,6,7,8,9, 1,2,3,4,5,6,7,8,9,10, 'a','b','c']
💡 性能真相 :很多人用
list_a + list_b拼接列表后再遍历,以为这样更「Pythonic」。但+操作符会分配一个全新的列表------两个百万级列表相加 = 新的两百万元素列表 = 大量内存分配。而chain()只创建了一个 56 字节 的 C 对象,按需从每个源依次取下一个值,O(1) 内存。对于数据管道来说,这个差距很多时候比 wall-clock time 更重要。
💡 踩坑提示 :上面的list(pipeline)已经耗尽 了这个迭代器。如果之后再next(pipeline)会立即抛出StopIteration。如果你需要多次遍历同一个 chain,有两个选择:① 重新创建它(chain()本身很便宜);② 先用list()物化(前提是数据量在内存可承受范围内)。
3.2 zip_longest():不等长迭代器的优雅配对
内置的 zip() 以最短的迭代器为准------短的那个耗尽后,剩余元素被丢弃。zip_longest() 以最长的为准,短的用 fillvalue 填充。
python
import itertools
list1 = [1, 2, 3, 4, 5]
list2 = ['a', 'b', 'c']
# 内置 zip:以最短为准
print(list(zip(list1, list2)))
# [(1, 'a'), (2, 'b'), (3, 'c')] ← 4 和 5 被丢弃了!
# zip_longest:以最长为尊
print(list(itertools.zip_longest(list1, list2)))
# [(1,'a'), (2,'b'), (3,'c'), (4,None), (5,None)]
# 自定义填充值
print(list(itertools.zip_longest(list1, list2, fillvalue="缺失")))
# [(1,'a'), (2,'b'), (3,'c'), (4,'缺失'), (5,'缺失')]
实用场景:
- 对齐多个不等长的时间序列数据
- 批处理的
grouper配方(利用zip_longest的锁步特性)
| 对比维度 | zip() |
zip_longest() |
|---|---|---|
| 对齐策略 | 最短优先 | 最长优先 |
| 短迭代器的剩余元素 | 丢弃 | 填充 fillvalue(默认 None) |
| 典型场景 | 等长数据对齐 | 不等长数据处理、批处理 |
3.3 islice():对迭代器切片,不复制数据
islice(iterable, start, stop, step) 像列表切片一样工作,但不创建中间结果。
python
import itertools
# 对 count() 这种无限迭代器切片------普通切片做不到
slice_iter = itertools.islice(itertools.count(start=0, step=2), 2, 7, 2)
print(list(slice_iter)) # [4, 8, 12]
# 对 range 切片
slice_iter2 = itertools.islice(range(10), 2, 7, 2)
print(list(slice_iter2)) # [2, 4, 6]
💡 什么时候用
islice而不是mylist[2:7:2]? 列表切片会创建一个新的列表副本,对于大文件的行迭代器或生成器,你做不到big_file[1000:2000]。而islice适用于任何可迭代对象,且零额外内存。唯一的代价:islice消费过的元素永远丢失了------它不会「倒回去」。
3.4 accumulate():不只是「累加」,而是「累积变换」
accumulate(iterable, func=operator.add) 返回每一步的累积结果。默认是累加,但传入任意二元函数就是任意累积变换。
python
import itertools
import operator
nums = [1, 2, 3, 4]
# 默认:累加
print(list(itertools.accumulate(nums)))
# [1, 3, 6, 10]
# 累乘
print(list(itertools.accumulate(nums, operator.mul)))
# [1, 2, 6, 24]
# 运行中取最大值
print(list(itertools.accumulate([4, 3, 2, 1, 8, 9, 5], max)))
# [4, 4, 4, 4, 8, 9, 9]
💡 纠偏观点 :很多教程把
accumulate称为「前缀和」,让人以为它只能做加法。实际上它的func参数接受任意二元函数------运行的乘积、最大值、最小值,甚至是自定义的 lambda(比如复利计算)。它是「累积变换」的通用工具,不是只能算和的专用函数。
python
# 高阶用法:复利摊销表
# 贷款 1000,年利率 5%,每年还款 90
cashflow = [1000, -90, -90, -90, -90]
amortization = itertools.accumulate(
cashflow,
lambda balance, payment: round(balance * 1.05 + payment, 2)
)
print(list(amortization))
# [1000, 960.0, 918.0, 873.9, 827.6]
3.5 Python 3.10+ / 3.12+ 新增利器
如果你的 Python 版本较新(3.10+),还有两个高频使用的迭代器值得了解。
pairwise()(3.10+):返回相邻元素的重叠对。常用于计算差值、变化率等。
python
import itertools
# 计算温度变化
temps = [19.0, 19.5, 20.1, 19.8]
changes = [b - a for a, b in itertools.pairwise(temps)]
print(changes) # [0.5, 0.6, -0.3]
batched()(3.12+):将数据分批为等长元组。数据库批量插入、API 分页的理想选择。
python
import itertools
records = range(1, 11)
for batch in itertools.batched(records, 4):
print(batch)
# (1, 2, 3, 4)
# (5, 6, 7, 8)
# (9, 10) ← 最后一批可能更短
4. 组合生成器:遍历搜索空间的利器
组合生成器用于生成排列、组合、笛卡尔积。它们最强大的场景是测试用例生成 和参数搜索------但也是最容易踩到内存陷阱的地方。
4.1 product():笛卡尔积------替代深层嵌套循环
product(*iterables, repeat=1) 生成输入可迭代对象的笛卡尔积。
python
import itertools
list1 = [1, 2, 3]
list2 = ['a', 'b', 'c']
# 单次迭代:等价于两层嵌套循环
result = list(itertools.product(list1, list2))
print(f"共 {len(result)} 个组合")
# 15 个组合:(1,'a'), (1,'b'), ..., (3,'c')
# repeat 参数:将同一个可迭代对象与自身做笛卡尔积
# product(A, A) 等价于 product(A, repeat=2)
⚠️ 危险警告 :
product(iterable, repeat=r)产生n^r个结果。10 个元素 repeat=4 = 10000,但 50 个元素 repeat=4 = 625 万。永远不要对不确定大小的输入调用list(product(...))------先用math.pow(n, r)估算结果数量。
4.2 permutations():全排列------顺序不同即不同
permutations(iterable, r=None) 生成所有可能的排列。排列数 = n! / (n-r)!。
python
import itertools
items = ['a', 'b', 'c']
# 全排列(r 默认 = len(items))
print(list(itertools.permutations(items)))
# [('a','b','c'), ('a','c','b'), ('b','a','c'),
# ('b','c','a'), ('c','a','b'), ('c','b','a')]
# 指定长度
print(list(itertools.permutations(items, 2)))
# [('a','b'), ('a','c'), ('b','a'), ('b','c'), ('c','a'), ('c','b')]
💡 我的踩坑实录 :
permutations(12, 12)= 479,001,600 个元组。如果全部物化为列表,每个 12 元素元组约 152 字节(64-bit CPython),总计约 72 GB 。在一台 8 GB 的机器上,这会直接触发 OOM Kill。黄金法则 :永远用filter()或islice在迭代器层面做筛选和截断,不要调list()。
4.3 combinations() / combinations_with_replacement():组合------元素内容不同即不同
combinations(iterable, r) 生成不重复的组合(顺序不重要)。combinations_with_replacement() 允许同一元素重复出现。
python
import itertools
items = ['a', 'b', 'c']
# 不重复组合(C(3,2) = 3 种)
print(list(itertools.combinations(items, 2)))
# [('a', 'b'), ('a', 'c'), ('b', 'c')]
# 可重复组合
print(list(itertools.combinations_with_replacement(items, 2)))
# [('a','a'), ('a','b'), ('a','c'), ('b','b'), ('b','c'), ('c','c')]
| 函数 | 关心顺序? | 可重复? | 结果数 | 典型场景 |
|---------------------------------------|:-----:|:----:|-----------------------|--------|---|---|----|--------|
| product(A, B) | --- | --- | ` | A | × | B | ` | 参数网格搜索 |
| permutations(A, r) | ✅ | ❌ | n!/(n-r)! | 测试输入顺序 |
| combinations(A, r) | ❌ | ❌ | n!/(r!(n-r)!) | 子集枚举 |
| combinations_with_replacement(A, r) | ❌ | ✅ | (n+r-1)!/(r!(n-1)!) | 有放回抽样 |
5. 实战场景:把 itertools 用到生产代码里
5.1 大文件批处理------islice + chain
处理几百 MB 的日志文件,一行一行读没问题------但当你需要分批写入数据库时,手动切片就很麻烦。
python
import itertools
def read_in_batches(filepath, batch_size=1000):
"""分批读取大文件,永不一次性加载全部内容"""
with open(filepath, 'r', encoding='utf-8') as f:
while True:
batch = list(itertools.islice(f, batch_size))
if not batch:
break
yield batch
# 使用示例
# for batch in read_in_batches("huge_log.txt", batch_size=500):
# db.bulk_insert(batch)
💡 关键点:
f本身就是一个行迭代器------islice(f, 1000)读取 1000 行后,文件指针自动前进。下次循环接着读下一批。整个过程只持有当前批次的内存。
5.2 测试用例自动生成------product + combinations
python
import itertools
# 参数网格:3 个参数 × 各 2-3 个取值 = 自动生成 12 个测试用例
databases = ["MySQL", "PostgreSQL"]
cache_modes = ["redis", "memcached", "none"]
timeout_values = [5, 30]
test_cases = list(itertools.product(databases, cache_modes, timeout_values))
print(f"共生成 {len(test_cases)} 个测试用例") # 12
# 每个用例:(db, cache, timeout) 三元组
5.3 多文件流式合并------chain.from_iterable
python
import itertools
import glob
def stream_all_logs(pattern="logs/*.log"):
"""合并所有日志文件为一个流,不产生中间列表"""
files = [open(f, 'r', encoding='utf-8') for f in glob.glob(pattern)]
return itertools.chain.from_iterable(files@[toc]
> 环境:Python 3.8+(`batched` 需 3.12+,`pairwise` 需 3.10+) | 适用人群:Python 初中级开发者、数据处理工程师
## 引言:为什么你需要掌握 itertools?
你是不是经常遇到这些场景:
- 处理几百万行日志文件,一个 `list()` 就把内存撑爆了
- 写一堆嵌套循环做排列组合,代码又长又容易出错
- 想合并多个数据源,却总在 `list_a + list_b` 时创建不必要的中间列表
- 跑测试时需要批量生成参数组合,手写循环写到怀疑人生
**itertools** 正是 Python 标准库为这些痛点准备的答案。它提供了一套高效、内存友好的迭代器构建块,核心优势只有一条:**只在需要时才产生下一个值**。
读完本文你将掌握:
1. 迭代器与可迭代对象的本质区别,以及为什么「一次性消费」是特性而非 bug
2. itertools 三大类函数(无限迭代器 / 有限迭代器 / 组合生成器)的 20+ 核心用法
3. 用 itertools 替代列表操作的性能收益------不只是「快一点」,而是 O(1) vs O(n) 的内存差距
4. 可直接复用的实战代码,以及我在生产环境中踩过的坑
> 💡 **我的踩坑实录**:第一次用 `chain()` 时,我以为它返回的是列表,在 `list(res_ch)` 之后又用 `next()` 遍历,直接抛了 `StopIteration`。排查半天才意识到:迭代器一旦耗尽就不能再次使用,这和列表可以反复遍历完全不同。从那以后,我每次用 itertools 都会问自己一句话:「这个东西还能迭代第二次吗?」
```mermaid
flowchart LR
subgraph 无限迭代器
A1[count]
A2[cycle]
A3[repeat]
end
subgraph 有限迭代器
B1[chain]
B2[zip_longest]
B3[islice]
B4[accumulate]
B5[batched 3.12+]
B6[pairwise 3.10+]
end
subgraph 组合生成器
C1[product]
C2[permutations]
C3[combinations]
C4[combinations_with_replacement]
end
无限迭代器 -->|需要 islice 截断| 有限迭代器
有限迭代器 -->|作为输入源| 组合生成器
组合生成器 -->|输出为迭代器| 有限迭代器
1. 迭代器基础:为什么「一次性」是好事?
在理解 itertools 之前,必须先搞清楚一个概念上的区分。
1.1 可迭代对象 vs 迭代器
在 Python 中,可迭代对象 (Iterable)是任何可以放进 for 循环的东西------列表、元组、字典、文件对象、生成器。它们实现了 __iter__() 方法。
迭代器 (Iterator)则是真正「产生值」的那个对象------它实现了 __next__() 方法,每次调用返回下一个元素,直到抛出 StopIteration。
python
# 可迭代对象 vs 迭代器
my_list = [1, 2, 3] # 可迭代对象,但不是迭代器
my_iter = iter(my_list) # 通过 iter() 获取迭代器
print(next(my_iter)) # 1
print(next(my_iter)) # 2
print(next(my_iter)) # 3
# print(next(my_iter)) # StopIteration ------ 迭代器已耗尽
💡 关键认知:列表可以遍历无数次,但迭代器只能消费一次。这不是 bug,而是内存效率的来源------迭代器不存储已产生的值,所以 O(1) 内存就能处理无限数据流。
1.2 itertools 的核心优势
itertools 的所有函数返回的都是迭代器。这意味着三个关键特性:
| 特性 | 含义 | 实际影响 |
|---|---|---|
| 惰性计算 | 只在 next() 调用时才生成下一个值 |
不会预先生成所有结果 |
| O(1) 内存 | 迭代器对象大小恒定(通常 56 字节) | 处理 1000 万行数据和 10 行数据内存占用相同 |
| 无限序列 | 可以表达无限长的数据流 | count() 永远不会停止,需要配合 islice 截断 |
2. 无限迭代器:永不停止的数据流
无限迭代器是 itertools 最独特的特性之一。它们可以无限产生元素 ------别担心,只要不用 list() 包裹它们,内存就不会爆炸。
2.1 count():数字的无限递增
count(start=0, step=1) 从 start 开始,以 step 为步长无限递增。它的底层是一个生成器,每次 next() 调用才计算下一个值。
python
import itertools
# 从 100 开始,每次增加 50
count_iter = itertools.count(start=100, step=50)
print(next(count_iter)) # 100
print(next(count_iter)) # 150
print(next(count_iter)) # 200
# 可以一直 next() 下去,永远不会停止
实用场景:
- 自动生成递增 ID:
next(itertools.count(1000)) - 配合
enumerate替代方案:zip(itertools.count(), data) - 模拟无限数据流用于测试
⚠️ 注意:
count()只能对数字递增。如果需要对任意可迭代对象做无限循环,请用cycle()。
2.2 cycle():无限循环任意可迭代对象
cycle(iterable) 会不断重复一个可迭代对象。到达末尾后自动回到开头------你永远不会看到 StopIteration。
python
import itertools
times = list(range(0, 12))
cycle_iter = itertools.cycle(times)
# 取前 15 个值------超过 12 后会从头循环
for _ in range(15):
print(next(cycle_iter), end=" ")
# 输出:0 1 2 3 4 5 6 7 8 9 10 11 0 1 2
💡 注意区分:
times是列表(可迭代对象但不是迭代器),所以对它next(times)会报TypeError: 'list' object is not an iterator。而cycle(times)返回的是一个迭代器,可以无限next()。这是前面 1.1 节那个「可迭代对象 vs 迭代器」区分在实战中的直接体现。
实用场景:
- 轮询调度(Round-Robin):
cycle(["server1", "server2", "server3"]) - 周期性任务标记:
cycle(["morning", "afternoon", "evening"]) - 生成重复的测试数据模式
⚠️ 注意:
cycle()会在内部缓存整个输入序列 (因为需要反复回绕)。如果输入是百万级列表,cycle()会一次性把它们全部缓存在内存中。这与 itertools「O(1) 内存」的一般承诺不同------对于大序列,考虑用count() % n替代。
2.3 repeat():重复同一个值
repeat(obj, times=None) 重复生成同一个对象。times 参数指定次数,不传则无限重复。
python
import itertools
# 重复 5 次
limited = itertools.repeat("任务完成", times=5)
print(list(limited))
# ['任务完成', '任务完成', '任务完成', '任务完成', '任务完成']
# 无限重复(需要配合 islice 截断)
infinite = itertools.repeat("心跳信号")
💡
repeat()的一个高阶用法是配合map():map(pow, range(10), repeat(2))计算 0² 到 9²,比[x**2 for x in range(10)]更具函数式风格------在需要将固定参数传入map的多参数函数时非常有用。
现在来看 repeat() 的一个重要细节------times 参数决定了迭代上限:
python
import itertools
limited = itertools.repeat("数据", times=5)
# 遍历 6 次------第 6 次会报错
for _ in range(6):
print(next(limited), end=" ")
# StopIteration ------ 第 6 次调用时迭代器已耗尽
3. 有限迭代器:数据流管道的基础组件
有限迭代器处理「有尽头」的数据。它们不会无限生成值,但同样享受惰性求值带来的内存优势。
3.1 chain():把多个可迭代对象串成一条流水线
chain(*iterables) 按顺序依次消费每个可迭代对象,返回一个连续的迭代器。
python
import itertools
source_1 = range(10)
source_2 = (x + 1 for x in source_1) # 生成器表达式
source_3 = ("a", "b", "c")
pipeline = itertools.chain(source_1, source_2, source_3)
print(list(pipeline))
# [0,1,2,3,4,5,6,7,8,9, 1,2,3,4,5,6,7,8,9,10, 'a','b','c']
💡 性能真相 :很多人用
list_a + list_b拼接列表后再遍历,以为这样更「Pythonic」。但+操作符会分配一个全新的列表------两个百万级列表相加 = 新的两百万元素列表 = 大量内存分配。而chain()只创建了一个 56 字节 的 C 对象,按需从每个源依次取下一个值,O(1) 内存。对于数据管道来说,这个差距很多时候比 wall-clock time 更重要。
💡 踩坑提示 :上面的list(pipeline)已经耗尽 了这个迭代器。如果之后再next(pipeline)会立即抛出StopIteration。如果你需要多次遍历同一个 chain,有两个选择:① 重新创建它(chain()本身很便宜);② 先用list()物化(前提是数据量在内存可承受范围内)。
3.2 zip_longest():不等长迭代器的优雅配对
内置的 zip() 以最短的迭代器为准------短的那个耗尽后,剩余元素被丢弃。zip_longest() 以最长的为准,短的用 fillvalue 填充。
python
import itertools
list1 = [1, 2, 3, 4, 5]
list2 = ['a', 'b', 'c']
# 内置 zip:以最短为准
print(list(zip(list1, list2)))
# [(1, 'a'), (2, 'b'), (3, 'c')] ← 4 和 5 被丢弃了!
# zip_longest:以最长为尊
print(list(itertools.zip_longest(list1, list2)))
# [(1,'a'), (2,'b'), (3,'c'), (4,None), (5,None)]
# 自定义填充值
print(list(itertools.zip_longest(list1, list2, fillvalue="缺失")))
# [(1,'a'), (2,'b'), (3,'c'), (4,'缺失'), (5,'缺失')]
实用场景:
- 对齐多个不等长的时间序列数据
- 批处理的
grouper配方(利用zip_longest的锁步特性)
| 对比维度 | zip() |
zip_longest() |
|---|---|---|
| 对齐策略 | 最短优先 | 最长优先 |
| 短迭代器的剩余元素 | 丢弃 | 填充 fillvalue(默认 None) |
| 典型场景 | 等长数据对齐 | 不等长数据处理、批处理 |
3.3 islice():对迭代器切片,不复制数据
islice(iterable, start, stop, step) 像列表切片一样工作,但不创建中间结果。
python
import itertools
# 对 count() 这种无限迭代器切片------普通切片做不到
slice_iter = itertools.islice(itertools.count(start=0, step=2), 2, 7, 2)
print(list(slice_iter)) # [4, 8, 12]
# 对 range 切片
slice_iter2 = itertools.islice(range(10), 2, 7, 2)
print(list(slice_iter2)) # [2, 4, 6]
💡 什么时候用
islice而不是mylist[2:7:2]? 列表切片会创建一个新的列表副本,对于大文件的行迭代器或生成器,你做不到big_file[1000:2000]。而islice适用于任何可迭代对象,且零额外内存。唯一的代价:islice消费过的元素永远丢失了------它不会「倒回去」。
3.4 accumulate():不只是「累加」,而是「累积变换」
accumulate(iterable, func=operator.add) 返回每一步的累积结果。默认是累加,但传入任意二元函数就是任意累积变换。
python
import itertools
import operator
nums = [1, 2, 3, 4]
# 默认:累加
print(list(itertools.accumulate(nums)))
# [1, 3, 6, 10]
# 累乘
print(list(itertools.accumulate(nums, operator.mul)))
# [1, 2, 6, 24]
# 运行中取最大值
print(list(itertools.accumulate([4, 3, 2, 1, 8, 9, 5], max)))
# [4, 4, 4, 4, 8, 9, 9]
💡 纠偏观点 :很多教程把
accumulate称为「前缀和」,让人以为它只能做加法。实际上它的func参数接受任意二元函数------运行的乘积、最大值、最小值,甚至是自定义的 lambda(比如复利计算)。它是「累积变换」的通用工具,不是只能算和的专用函数。
python
# 高阶用法:复利摊销表
# 贷款 1000,年利率 5%,每年还款 90
cashflow = [1000, -90, -90, -90, -90]
amortization = itertools.accumulate(
cashflow,
lambda balance, payment: round(balance * 1.05 + payment, 2)
)
print(list(amortization))
# [1000, 960.0, 918.0, 873.9, 827.6]
3.5 Python 3.10+ / 3.12+ 新增利器
如果你的 Python 版本较新(3.10+),还有两个高频使用的迭代器值得了解。
pairwise()(3.10+):返回相邻元素的重叠对。常用于计算差值、变化率等。
python
import itertools
# 计算温度变化
temps = [19.0, 19.5, 20.1, 19.8]
changes = [b - a for a, b in itertools.pairwise(temps)]
print(changes) # [0.5, 0.6, -0.3]
batched()(3.12+):将数据分批为等长元组。数据库批量插入、API 分页的理想选择。
python
import itertools
records = range(1, 11)
for batch in itertools.batched(records, 4):
print(batch)
# (1, 2, 3, 4)
# (5, 6, 7, 8)
# (9, 10) ← 最后一批可能更短
4. 组合生成器:遍历搜索空间的利器
组合生成器用于生成排列、组合、笛卡尔积。它们最强大的场景是测试用例生成 和参数搜索------但也是最容易踩到内存陷阱的地方。
4.1 product():笛卡尔积------替代深层嵌套循环
product(*iterables, repeat=1) 生成输入可迭代对象的笛卡尔积。
python
import itertools
list1 = [1, 2, 3]
list2 = ['a', 'b', 'c']
# 单次迭代:等价于两层嵌套循环
result = list(itertools.product(list1, list2))
print(f"共 {len(result)} 个组合")
# 15 个组合:(1,'a'), (1,'b'), ..., (3,'c')
# repeat 参数:将同一个可迭代对象与自身做笛卡尔积
# product(A, A) 等价于 product(A, repeat=2)
⚠️ 危险警告 :
product(iterable, repeat=r)产生n^r个结果。10 个元素 repeat=4 = 10000,但 50 个元素 repeat=4 = 625 万。永远不要对不确定大小的输入调用list(product(...))------先用math.pow(n, r)估算结果数量。
4.2 permutations():全排列------顺序不同即不同
permutations(iterable, r=None) 生成所有可能的排列。排列数 = n! / (n-r)!。
python
import itertools
items = ['a', 'b', 'c']
# 全排列(r 默认 = len(items))
print(list(itertools.permutations(items)))
# [('a','b','c'), ('a','c','b'), ('b','a','c'),
# ('b','c','a'), ('c','a','b'), ('c','b','a')]
# 指定长度
print(list(itertools.permutations(items, 2)))
# [('a','b'), ('a','c'), ('b','a'), ('b','c'), ('c','a'), ('c','b')]
💡 我的踩坑实录 :
permutations(12, 12)= 479,001,600 个元组。如果全部物化为列表,每个 12 元素元组约 152 字节(64-bit CPython),总计约 72 GB 。在一台 8 GB 的机器上,这会直接触发 OOM Kill。黄金法则 :永远用filter()或islice在迭代器层面做筛选和截断,不要调list()。
4.3 combinations() / combinations_with_replacement():组合------元素内容不同即不同
combinations(iterable, r) 生成不重复的组合(顺序不重要)。combinations_with_replacement() 允许同一元素重复出现。
python
import itertools
items = ['a', 'b', 'c']
# 不重复组合(C(3,2) = 3 种)
print(list(itertools.combinations(items, 2)))
# [('a', 'b'), ('a', 'c'), ('b', 'c')]
# 可重复组合
print(list(itertools.combinations_with_replacement(items, 2)))
# [('a','a'), ('a','b'), ('a','c'), ('b','b'), ('b','c'), ('c','c')]
| 函数 | 关心顺序? | 可重复? | 结果数 | 典型场景 |
|---------------------------------------|:-----:|:----:|-----------------------|--------|---|---|----|--------|
| product(A, B) | --- | --- | ` | A | × | B | ` | 参数网格搜索 |
| permutations(A, r) | ✅ | ❌ | n!/(n-r)! | 测试输入顺序 |
| combinations(A, r) | ❌ | ❌ | n!/(r!(n-r)!) | 子集枚举 |
| combinations_with_replacement(A, r) | ❌ | ✅ | (n+r-1)!/(r!(n-1)!) | 有放回抽样 |
5. 实战场景:把 itertools 用到生产代码里
5.1 大文件批处理------islice + chain
处理几百 MB 的日志文件,一行一行读没问题------但当你需要分批写入数据库时,手动切片就很麻烦。
python
import itertools
def read_in_batches(filepath, batch_size=1000):
"""分批读取大文件,永不一次性加载全部内容"""
with open(filepath, 'r', encoding='utf-8') as f:
while True:
batch = list(itertools.islice(f, batch_size))
if not batch:
break
yield batch
# 使用示例
# for batch in read_in_batches("huge_log.txt", batch_size=500):
# db.bulk_insert(batch)
💡 关键点:
f本身就是一个行迭代器------islice(f, 1000)读取 1000 行后,文件指针自动前进。下次循环接着读下一批。整个过程只持有当前批次的内存。
5.2 测试用例自动生成------product + combinations
python
import itertools
# 参数网格:3 个参数 × 各 2-3 个取值 = 自动生成 12 个测试用例
databases = ["MySQL", "PostgreSQL"]
cache_modes = ["redis", "memcached", "none"]
timeout_values = [5, 30]
test_cases = list(itertools.product(databases, cache_modes, timeout_values))
print(f"共生成 {len(test_cases)} 个测试用例") # 12
# 每个用例:(db, cache, timeout) 三元组
5.3 多文件流式合并------chain.from_iterable
python
import itertools
import glob
def stream_all_logs(pattern="logs/*.log"):
"""合并所有日志文件为一个流,不产生中间列表"""
files = [open(f, 'r', encoding='utf-8') for f in glob.glob(pattern)]
return itertools.chain.from_iterable(files)
# 使用后记得关文件------生产代码建议用 contextlib.ExitStack
总结
三个核心收获
- 惰性求值是 itertools 的灵魂 :只在
next()调用时产生下一个值,O(1) 内存处理无限数据流。这个特性不是「性能优化」,而是从「先建列表再处理」到「流式管道」的思维转变。 chain的内存优势往往比速度优势更重要 :list_a + list_b分配新列表,chain只持有两个引用(56 字节)。在数据管道场景下,内存稳定性比微观速度更有价值。- 组合生成器有「内存炸弹」风险 :
permutations(12,12)= 479M tuples ≈ ~72GB。永远在用list()物化之前,用math.perm/math.comb估算结果规模。
完整代码汇总
以上所有代码块均可独立运行(Python 3.8+)。建议按以下顺序练习:
- 先用
count / cycle / repeat感受无限迭代器 - 再用
chain / zip_longest / islice / accumulate构建数据管道 - 最后用
product / permutations / combinations生成测试用例,体会「组合爆炸」的威力
一个开放问题
itertools 的所有函数返回的是迭代器而不是列表,这个设计带来了 O(1) 内存的优势,但也引入了「一次性消费」的限制。如果你的下游代码需要多次遍历同一个结果,你会选择:
- A)先用
list()物化(接受内存成本) - B)每次重新创建迭代器管道(接受 CPU 成本)
- C)用
itertools.tee()克隆迭代器(注意:tee内部也会缓存数据)
你的场景是什么?在评论区聊聊你的选择。
)
使用后记得关文件------生产代码建议用 contextlib.ExitStack
markdown
## 总结
### 三个核心收获
- **惰性求值是 itertools 的灵魂**:只在 `next()` 调用时产生下一个值,O(1) 内存处理无限数据流。这个特性不是「性能优化」,而是从「先建列表再处理」到「流式管道」的思维转变。
- **`chain` 的内存优势往往比速度优势更重要**:`list_a + list_b` 分配新列表,`chain` 只持有两个引用(56 字节)。在数据管道场景下,内存稳定性比微观速度更有价值。
- **组合生成器有「内存炸弹」风险**:`permutations(12,12)` = 479M tuples ≈ ~72GB。永远在用 `list()` 物化之前,用 `math.perm` / `math.comb` 估算结果规模。
### 完整代码汇总
以上所有代码块均可独立运行(Python 3.8+)。建议按以下顺序练习:
1. 先用 `count / cycle / repeat` 感受无限迭代器
2. 再用 `chain / zip_longest / islice / accumulate` 构建数据管道
3. 最后用 `product / permutations / combinations` 生成测试用例,体会「组合爆炸」的威力
### 一个开放问题
itertools 的所有函数返回的是**迭代器**而不是列表,这个设计带来了 O(1) 内存的优势,但也引入了「一次性消费」的限制。如果你的下游代码需要多次遍历同一个结果,你会选择:
- A)先用 `list()` 物化(接受内存成本)
- B)每次重新创建迭代器管道(接受 CPU 成本)
- C)用 `itertools.tee()` 克隆迭代器(注意:`tee` 内部也会缓存数据)
你的场景是什么?在评论区聊聊你的选择。