在 Python 标准库里itertools 是那种“看见的人少真用起来惊艳”的模块。它不给数据提供容器而是把数据当成水管里的水流用一系列迭代器工具帮你控流量、做分流、任意裁剪。很多写过大数据量处理脚本的人最后都会回到这套组合方案上原因只有一个它真的省内存、省代码、省心。我最早被 itertools 救了一次是在做一份百万行级的日志分析任务。当时老方案是先全部读进列表再筛服务内存直接飙到两三百兆卡到没法看改成链式迭代器方案后代码缩短一大截内存峰值几乎可以忽略。这个模块解决的核心问题就是把“一次性全量加工”变成“一口肉一口吃”同时把最常用的一批循环套路全部函数化。这篇文章适合所有用 Python 写数据处理、接口解析、爬虫、自动化脚本的人我会按工具分组逐个讲清用途、原理、参数细节再附上我真实踩过的坑和排查思路争取你读完就能在项目里直接套用。1. 无限迭代器count、cycle、repeat 的正确打开方式无限迭代器组在数据流场景里最常用共同特点是“没有终止条件”能跑多久完全由消费者决定。使用时的第一原则是永远别让我来兜底一定要在取用端限制次数。1.1 count 不只是计数器更是坐标生成器count(start0, step1)会从 start 开始按 step 无限递增。很多人以为它只能生成自然数其实 step 传小数、负数都没问题。比如count(0, 0.5)可以生成 0、0.5、1.0、1.5……这种浮点步进做数值模拟时很顺手。基础写法是这样from itertools import count for idx in count(10, 3): if idx 25: break print(idx)输出 10、13、16、19、22、25。注意这里我用break手动结束你也可以换成islice(count(10, 3), 6)来限量后续章节会细讲。日常场景里count最常见的身份是“自动编号器”。很多人习惯用enumerate给数据补序号但那样只能从 0 且步长为 1当你想让编号从某个指定值开始、或者按固定间隔跳号count的表达力明显更强from itertools import count logs [a, b, c] numbered list(zip(count(100, 2), logs))结果是[(100, a), (102, b), (104, c)]。这一招做导出文件的“行号列”特别好用不用单独维护一个累加变量。有一点要克制count和所有无限迭代器一样直接放进for循环而忘记写退出条件就会变成死循环。我见过同事在交互环境里跑了一个count()然后控制台疯狂刷屏最后只能中断进程。开发调试阶段建议永远先用限量工具包一层。1.2 cycle 让数据无限循环但要注意内存cycle(iterable)会把传入的可迭代序列记住然后无限循环输出。红绿灯状态切换、轮询负载均衡、奇偶行染色这类“周期性重复”的需求用它是标准解。典型的负载均衡轮询from itertools import cycle nodes [node-a, node-b, node-c] for addr in cycle(nodes): # 在这里发起请求 pass这个写法比手写i % len(nodes)再取下标要清晰得多而且在循环体内不需要关心下标越界。但有两个注意点。第一cycle会在内部保留一份传入序列的副本。如果你拿它去循环一个特别大的列表内存占用不会因为你用了迭代器而减少那一份拷贝是躲不掉的。第二如果传入的对象本身是无限迭代器比如cycle(count())这会让嵌套失去意义——源数据根本没有尽头循环也谈不上状态切换。这种情况请直接用源迭代器本身。1.3 repeat 没那么简单repeat(object, timesNone)的效果是不断返回同一个对象。最容易忽略的有两件事。第一times缺省时它会无限重复同样不要裸用。第二它重复的是“同一个引用”不是拷贝。如果对象是可变容器后面改了某个元素前面“重复”出来的内容也会一起变from itertools import repeat x [1, 2] r list(repeat(x, 3)) x.append(3) print(r) # [[1, 2, 3], [1, 2, 3], [1, 2, 3]]所以如果你需要的是多个互相独立的容器repeat并不合适请用列表推导式。repeat真正值钱的地方是和map配合用来“补齐参数”。比如要计算1^2, 2^2, 3^2, ...可以这样from itertools import repeat list(map(pow, range(1, 5), repeat(2))) # [1, 4, 9, 16]这里repeat(2)不断提供第二个参数map依次从两个迭代器取参直到短的耗尽。这种写法避免了在每次调用里显式构造(x, 2)的元组代码更干净。单纯想生成一个重复列表时[x] * n确实更直观但repeat作为参数传入时不需要落地成序列内存占用少这个优势在超大次数场景下是决定性的。2. 切片与筛选islice、takewhile、dropwhile、filterfalse无限迭代器提供了源头但实战中我们需要的是“限量取用”和“按条件筛选”。这一组工具解决的就是这两件事。2.1 islice迭代器也能切片普通迭代器不支持下标访问比如it[2:5]会直接报错。islice(iterable, start, stop, step)把切片的语义带给了迭代器。三种写法对应不同需求islice(it, stop)取前 stop 个元素islice(it, start, stop)取中间一段islice(it, start, stop, step)带步长取一段from itertools import islice for x in islice(range(100), 10, 30, 3): print(x, end )输出10 13 16 19 22 25 28。含义和列表切片一致但有一个关键差异所有参数只能是正数不支持负数索引。因为迭代器本质上只能向前走没法从尾部往回退。如果你想“取倒数 5 个”得先把数据变成列表或者用其他方案。真实场景中islice最常用的就是“只需要读文件前 N 行”from itertools import islice with open(big.log, r, encodingutf-8) as f: head list(islice(f, 5))这里避免了把整个文件读进内存。注意islice内部为了跳到start位置会逐个消费并丢弃前面的元素。如果start很大比如一百万那它依然会实打实迭代一百万次时间成本并不低只是省了内存。2.2 takewhile 与 dropwhile 的语义差别takewhile(predicate, iterable)从头开始取满足条件的元素遇到第一个不满足就停止。dropwhile(predicate, iterable)则反过来从头开始丢弃满足条件的元素遇到第一个不满足就保留后续所有元素。关键点在于“遇到第一个不满足就停止”和“遇到第一个不满足就转正”这个语义它们处理的是前缀不是全量。例如解析带文件头的日志from itertools import dropwhile lines [# header, # author, data-1, data-2] body list(dropwhile(lambda x: x.startswith(#), lines))结果就直接拿到了去掉文件头的正文部分。用filter也可以做到“去掉#开头行”但二者语义完全不同filter会扫过全部数据把中间任何位置满足条件的行统统过滤掉dropwhile只认前缀前几行是头就丢前几行后面就算再出现#开头也会原样保留。选哪个取决于你想要的业务含义。2.3 filterfalse 反向筛选filterfalse(predicate, iterable)返回所有predicate为假值的元素相当于filter的取反版本。from itertools import filterfalse even list(filterfalse(lambda x: x % 2, range(10))) # [0, 2, 4, 6, 8]如果predicate传None它会直接过滤掉所有假值比如0、空字符串、None、空列表。清理用户输入里的空行时非常方便clean list(filterfalse(None, lines))我实际看代码时发现很多新人会手写[x for x in lines if not x]效果虽然一样但filterfalse(None, ...)的表意更直接而且依然是惰性求值适合流式处理大数据。3. 合并与分组chain、zip_longest、groupby 实战处理多条数据流时我们经常要拼接、对齐、分组。itertools在这里给的不是魔法而是几个能代替手写复杂循环的标准件。3.1 chain 拼接与压平chain(*iterables)把多个可迭代对象串成一条流不会把元素复制进新列表。和list1 list2相比内存优势非常明显尤其是数据量大的时候。from itertools import chain a [1, 2] b (3, 4) c [5] for x in chain(a, b, c): print(x, end ) # 1 2 3 4 5嵌套列表压平可以用chain.from_iterablefrom itertools import chain nested [[1, 2], [3], [4, 5]] flat list(chain.from_iterable(nested)) # [1, 2, 3, 4, 5]需要注意chain只能压一层。如果你有两种类型的嵌套层级比如[[[1,2]], [3]]就需要重复使用或者另写递归。它不是一个通用递归展平器别指望它做魔改操作。3.2 zip_longest 处理不等长序列普通zip按最短序列截断而zip_longest按最长序列补齐。这个差异在数据和字段对齐时非常关键。from itertools import zip_longest list(zip_longest(ab, 1234, fillvalue-)) # [(a, 1), (b, 2), (-, 3), (-, 4)]如果不指定fillvalue默认用None填充。后续如果马上要对值做运算None很可能引发TypeError。所以只要不是明确要保留空位我建议总是显式给出fillvalue。多列报表导出、键值对补齐、把不同长度的接口返回拼成统一格式这几个场景里zip_longest都是不错的选择。3.3 groupby 分组的三个大坑groupby(iterable, keyNone)会把连续且 key 相同的元素归成一组返回(key, group_iterator)。第一反应觉得这和 SQL 的 GROUP BY 很像但实际使用时坑非常多。第一个坑它只认连续。如果不先排序相同 key 的元素隔开了就会被拆成多个组。SQL 的 GROUP BY 会自动聚集Python 的groupby不会。from itertools import groupby data [1, 1, 2, 1, 2, 2] for k, g in groupby(data): print(k, list(g))输出是1 [1,1]、2 [2]、1 [1]、2 [2,2]——同 key 被拆成两组。想按 key 聚合必须先把数据按同一个 key 排序好data sorted([1, 1, 2, 1, 2, 2]) for k, g in groupby(data): print(k, list(g))第二个坑group_iterator的生命周期很短。它只在当前组处于活动状态时有效一旦groupby移动到下一组前一个组的迭代器就失效了。所以如果你要保存组内容必须马上转成listresult [] for k, g in groupby(data): result.append((k, list(g)))如果直接保存g等循环结束回头再看每个组都是空的。这个问题我见过太多次。第三个坑分组时一定要记得 key 函数保持一致。你按什么字段排序就要按什么字段分组否则排序白排。实际业务中交易记录按日期分组这种事通常会搭配operator.itemgetterfrom itertools import groupby from operator import itemgetter records [ {date: 2024-01-01, amount: 10}, {date: 2024-01-01, amount: 20}, {date: 2024-01-02, amount: 5}, ] records.sort(keyitemgetter(date)) for date, items in groupby(records, keyitemgetter(date)): total sum(item[amount] for item in items) print(date, total)拿日期作为 key就能快速得到每天的汇总金额全程不用手写字典累加逻辑。4. 组合排列product、permutations、combinations如果说前面几组工具是在处理“单条数据流”那这一组就是在做“多条数据流之间的所有可能性组合”。写测试用例、生成参数矩阵、做枚举搜索都离不开它们。4.1 product 笛卡尔积product(*iterables, repeat1)生成所有笛卡尔积等价于多层嵌套的for循环。from itertools import product for combo in product([A, B], [1, 2]): print(combo) # (A, 1) # (A, 2) # (B, 1) # (B, 2)repeat参数表示“自身重复几次”比如product(AB, repeat3)等价于product(AB, AB, AB)。多层嵌套循环写成product后代码平铺化层级感更清晰。一个典型场景是测试数据全组合from itertools import product sizes [S, M, L] colors [red, blue] materials [cotton, wool] for size, color, material in product(sizes, colors, materials): # 生成一套测试用例 pass手写三重循环也能完成但一旦层级增加到四层五层缩进就很痛苦。product是这类场景的标准答案。4.2 permutations 排列permutations(iterable, rNone)生成从 n 个元素中取 r 个的“有序排列”。r默认等于 n也就是全排列。所谓有序就是(a, b)和(b, a)被视为两种不同结果。from itertools import permutations list(permutations(abc, 2)) # [(a, b), (a, c), (b, a), (b, c), (c, a), (c, b)]排列适合那些顺序本身就有意义的场景比如“从 3 个城市里挑 2 个先后顺序不同代表去程返程不同”。拼车路线、任务调度、排序结果穷举都属于这一类。4.3 combinations 组合combinations(iterable, r)生成“无序组合”。同一个集合内部元素不重复且输出顺序保持原可迭代对象的顺序。from itertools import combinations list(combinations(abc, 2)) # [(a, b), (a, c), (b, c)]这里不存在(b, a)因为组合不考虑顺序。适用场景包括从特征列表里挑两两组合做关联分析、从一组节点里选若干节点做抽样、从一个班级里选代表。4.4 combinations_with_replacementcombinations_with_replacement(iterable, r)允许同一个元素被重复选择。它和combinations的区别在于(a, a)这种组合是合法的但依然不考虑顺序。from itertools import combinations_with_replacement list(combinations_with_replacement(AB, 2)) # [(A, A), (A, B), (B, B)]选择困难时可以看这张表工具是否考虑顺序是否允许重复数量公式product是是n^rpermutations是否n! / (n-r)!combinations否否n! / (r! × (n-r)!)combinations_with_replacement否是(nr-1)! / (r! × (n-1)!)写代码前先估算数量很重要。比如从 20 个元素里取 10 个combinations的数量是 184756内存上还能接受但如果用permutations数量会超过 6 亿直接list()会把内存打爆。任何组合生成器只要数量预估超过百万级别我都建议用循环即时消费不要尝试一次性落地成列表。5. 高级工具accumulate、starmap、tee前面几组工具解决的都是“常见场景”但 itertools 里还有几个不那么常见、却能在特定场景下把代码写得特别漂亮的工具。5.1 accumulate 累积计算accumulate(iterable, funcoperator.add)逐个累积返回每一步的中间结果。默认是累加from itertools import accumulate list(accumulate([1, 2, 3, 4])) # [1, 3, 6, 10]可以换成乘、取最大值、取最小值。比如看一笔流水中“曾经的最大余额”from itertools import accumulate balances [100, -50, 200, -80] list(accumulate(balances, max)) # [100, 100, 200, 200]这种“运行中聚合值”是财务报表和监控系统里的常见需求。手写的话要多维护一个临时变量写起来不复杂但用accumulate能把意图表达得更明确尤其适合在函数式风格的数据处理管道里直接调用。5.2 starmap 解包传参starmap(func, iterable)对迭代器里的每个元素执行func(*element)也就是先把元组解包成多个参数再传给函数。from itertools import starmap data [(1, 2), (3, 4)] list(starmap(lambda x, y: x * x y * y, data)) # [5, 25]对比一下mapmap(func, a, b)是从多个迭代器中各取一个元素然后传给函数starmap是一个迭代器里的每个元素按结构拆开传参。两者形态不同别用混。当我有一个(x, y)坐标列表需要计算到原点的距离时starmap能把代码写得很紧凑。5.3 tee 克隆迭代器tee(iterable, n2)把一个迭代器复制成 n 个独立迭代器可以分别消费而不互相干扰。from itertools import tee it1, it2 tee(range(10), 2) print(list(it1)) print(list(it2)) # [0, 1, 2, ..., 9] # [0, 1, 2, ..., 9]实现原理是内部为每个克隆出来的迭代器维护一个缓冲队列。源迭代器被推进时数据会被缓存到多个队列里。所以n不要开太大否则内存会随着缓存的快照数量膨胀。如果源迭代器本身是文件逐行读取使用tee时意味着一部分行内容会被暂存在内存里请确保在内存充裕的场景下使用。5.4 组合拳小案例把工具拼起来看才是 itertools 真正的使用方式。比如这样一个需求从一个大日志文件里提取前 100 条非空行并给它们加上行号。from itertools import chain, islice, count with open(big.log, r, encodingutf-8) as f: lines (line.strip() for line in f) non_empty filter(None, lines) numbered zip(count(1), islice(non_empty, 100)) for no, line in numbered: pass这段代码没有把整个文件读进列表也没有一次性生成全部编号行。islice(non_empty, 100)保证最多取 100 条count(1)提供连续编号zip把二者粘合。整个管道里每一步都是惰性的文件只有被消费到的行才会被真正读出。这就是 itertools 组合拳的典型节奏管道式思考不落地中间结果。6. 常见问题排查与性能优化实录最后把这几年用 itertools 踩过的坑集中总结一下这些坑在官方文档里基本不会提示但实战里几乎每个人都会撞上其中一两个。6.1 迭代器一次性使用迭代器被消费完后就是空的。最典型的症状是第一次for循环有数据第二次再遍历同一个迭代器什么都没了。groupby返回的group也同理被消费后再取就是空。排查这种问题先看看自己是不是把同一个迭代器对象用在了多处。如果需要重复遍历正确的做法是重新生成一次迭代器或者用tee提前复制。6.2 无限迭代器与 for 循环的组合失控for item in count():如果没写break就会无限循环。调试阶段尤其危险控制台会被疯狂刷屏。我现在写无限迭代器相关代码时都会先加一个islice限制from itertools import count, islice for item in islice(count(), 100): # 先跑通再说 pass等逻辑验证完毕再决定要不要放开限制。这个习惯能避免大部分失控事故。6.3 groupby 未排序导致分组错乱上一篇已经反复提过groupby只认连续不自动排序。如果你直接对一个没有排序的列表做分组相同的 key 会散落成多个组。数据一旦达到上万行这种错误非常隐蔽——因为结果不是报错而是“多出来很多组”或者“统计值偏低”。排查时先打印一下分组结果看同 key 是否连续出现如果不连续先排序再用。6.4 盲目调用 list() 打爆内存迭代器最大的价值是惰性求值很多人却习惯写完迭代器立刻list()看一眼结果。在大数据量场景下这一步可能直接导致内存溢出。要记住list()只是调试辅助手段不是管道的一部分。想看内容可以用list(islice(it, 10))只取前 10 个既能看到结果又不会把整条生成器耗尽。我实测过一个 100 万行日志文件的过滤场景如果用列表推导加过滤内存峰值能到 200 多兆改成filter加islice的迭代流方案内存峰值不到 1 兆处理时间也几乎持平。这就是为什么说“能迭代就不落地”不只是一句口号而是实打实的性能决策。6.5 用运算符模块配合更省事itertools经常和operator模块一起出现。比如groupby的 key 函数用itemgetter(date)accumulate的算子用operator.mul、operator.add都比写lambda更清晰也更快。这个小习惯在数据和参数多的时候会让代码可读性提升一个台阶。我的日常心法大概是这样的处理超过几十万行的数据第一步先问自己能不能改成迭代器管道写多层循环嵌套时第一步先想product或者combinations能不能替代调试迭代器时不盲猜打印结果直接list(islice(it, n))看前 n 个元素。itertools 的函数单独看都不复杂真正难的是组合使用。它们就像乐高基础件单看是一个小方块拼起来能搭出整套数据处理管线而且搭出来的东西内存占用低、逻辑清晰、几乎没有多余步骤。