Python itertools 深度指南:用迭代器代数写出更高效的代码

@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 都会问自己一句话:「这个东西还能迭代第二次吗?」

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 截断
flowchart TD A[数据源<br/>文件/列表/生成器] --> B[itertools 迭代器管道] B --> C[count/cycle/repeat<br/>无限生成] B --> D[chain/islice/accumulate<br/>有限变换] B --> E[product/combinations<br/>组合展开] C --> F{需要截断?} F -->|是| G[islice 切片] F -->|否| H[for 循环 break] D --> I[最终消费<br/>list/sum/for] E --> I G --> I H --> I

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()

flowchart TD A[需要生成排列/组合] --> B{输入大小已知?} B -->|否| C[⚠️ 先用 math.perm/math.comb\n估算结果数量] B -->|是| D{结果数量 < 10万?} D -->|是| E[可以 list 物化] D -->|否| F{结果数量 < 100万?} F -->|是| G[用 islice 截断\n或用 filter 筛选] F -->|否| H{结果数量 > 1000万?} H -->|是| I[🚨 绝对不要物化!\n必须在迭代器层面处理] C --> D

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 截断
flowchart TD A[数据源<br/>文件/列表/生成器] --> B[itertools 迭代器管道] B --> C[count/cycle/repeat<br/>无限生成] B --> D[chain/islice/accumulate<br/>有限变换] B --> E[product/combinations<br/>组合展开] C --> F{需要截断?} F -->|是| G[islice 切片] F -->|否| H[for 循环 break] D --> I[最终消费<br/>list/sum/for] E --> I G --> I H --> I

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()

flowchart TD A[需要生成排列/组合] --> B{输入大小已知?} B -->|否| C[⚠️ 先用 math.perm/math.comb\n估算结果数量] B -->|是| D{结果数量 < 10万?} D -->|是| E[可以 list 物化] D -->|否| F{结果数量 < 100万?} F -->|是| G[用 islice 截断\n或用 filter 筛选] F -->|否| H{结果数量 > 1000万?} H -->|是| I[🚨 绝对不要物化!\n必须在迭代器层面处理] C --> D

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+)。建议按以下顺序练习:

  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 内部也会缓存数据)

你的场景是什么?在评论区聊聊你的选择。

)

使用后记得关文件------生产代码建议用 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` 内部也会缓存数据)

你的场景是什么?在评论区聊聊你的选择。
相关推荐
小蜜蜂dry2 小时前
nestjs实战-权限二:角色模块
前端·后端·nestjs
默默且听风2 小时前
Ubuntu 22 环境下 VS Code Codex 插件无法打开的排查与修复记录
后端·ai编程·vibecoding
小蜜蜂dry2 小时前
nestjs实战-权限一: 菜单模块
前端·后端·nestjs
BingoGo3 小时前
PHP 在领域驱动(DDD)设计中的核心实践
后端·php
掘金者阿豪4 小时前
终于!我的第二本书正式出版,吃透 Agentic AI 核心不踩坑
javascript·后端
二月龙4 小时前
Redis 缓存设计避坑指南:穿透、击穿、雪崩与一致性问题
后端
掘金者阿豪4 小时前
运营不会SQL怎么办?我把数据库变成了大家都会用的表格
后端
孟陬4 小时前
国外技术周刊 #139:LLM 正在杀死程序员的「懒惰美德」
前端·人工智能·后端
七牛云行业应用4 小时前
Codex CLI 和 Codex 桌面端完整教程:两种入口的功能对比与选择指南
前端·后端·github