Python itertools 实战:用 groupby、chain、islice 优雅处理大数据流

Python itertools 实战:用 groupby、chain、islice 优雅处理大数据流

写数据处理脚本时,你是不是经常这么干:先把整个文件读进一个 list,再用 for 循环套 if 分组、拼接、切片?数据量小没事,一旦文件几个 G,内存直接爆掉。Python 标准库里的 itertools 就是专门解决这个的------它让你像流水线一样处理数据,内存占用几乎恒定。这篇挑三个最实用的函数,配真实场景讲清楚怎么用、坑在哪。

场景一:groupby 分组,但它有个致命前提

假设你有一份访问日志,要按用户 ID 统计每人的访问次数。很多人第一反应是用 defaultdict,但如果日志已经按用户排好序,groupby 更省内存------它不需要把所有数据攒在字典里。

先看一个新手必踩的坑:

python 复制代码
from itertools import groupby

logs = [
    ("alice", "/home"),
    ("alice", "/profile"),
    ("bob", "/home"),
    ("alice", "/settings"),  # 注意:alice 又出现了,但和前面不连续
]

for user, group in groupby(logs, key=lambda x: x[0]):
    print(user, len(list(group)))

输出是:

复制代码
alice 2
bob 1
alice 1

alice 被拆成了两组!因为 groupby 只会合并「相邻」的相同 key ,它不是全局分组。这是它和 SQL 的 GROUP BY 最大的区别,也是最容易出错的地方。

正确用法是先排序,再分组:

python 复制代码
from itertools import groupby

logs = [
    ("alice", "/home"),
    ("alice", "/profile"),
    ("bob", "/home"),
    ("alice", "/settings"),
]

# 关键:排序的 key 必须和 groupby 的 key 一致
logs.sort(key=lambda x: x[0])

for user, group in groupby(logs, key=lambda x: x[0]):
    paths = [path for _, path in group]
    print(f"{user}: {len(paths)} 次访问 -> {paths}")

输出正确了:

复制代码
alice: 3 次访问 -> ['/home', '/profile', '/settings']
bob: 1 次访问 -> ['/home']

还有一个隐蔽坑:group 是个迭代器,只能消费一次 。如果你先 len(list(group)) 再想遍历它,第二次拿到的就是空的。要么存成 list 复用,要么一次性处理完。

场景二:chain 把多个数据源接成一条流

你有三个日志文件要合并处理,朴素写法是读进三个 list 再相加:

python 复制代码
# 朴素写法:三个文件全读进内存再拼接,内存 = 三个文件之和
all_lines = list(open("a.log")) + list(open("b.log")) + list(open("c.log"))
for line in all_lines:
    process(line)

chain 能把多个可迭代对象「首尾相接」成一个,全程惰性求值,任意时刻内存里只有一行:

python 复制代码
from itertools import chain

files = ["a.log", "b.log", "c.log"]

# chain.from_iterable 接收「可迭代对象的可迭代对象」
# 每个文件对象本身就是行的迭代器,读一行处理一行
lines = chain.from_iterable(open(f) for f in files)

for line in lines:
    process(line.strip())

chain(a, b, c) 是把参数一个个接起来,chain.from_iterable(iterables) 则是把一个装着多个可迭代对象的序列拉平------处理动态数量的数据源时用后者,不用手动 * 解包。

场景三:islice 给无限流「切一刀」

itertools 里很多函数返回的是无限迭代器,比如 count()(从 0 数到无穷)。想取前 N 个怎么办?你不能用 list(count())[:10],那会把内存撑爆。用 islice------它是「迭代器版的切片」:

python 复制代码
from itertools import count, islice

# count(1, 2) 生成 1, 3, 5, 7... 无限奇数流
odds = count(1, 2)

# islice 取前 5 个,不会真的去遍历无限流
first_five = list(islice(odds, 5))
print(first_five)  # [1, 3, 5, 7, 9]

islice 最实用的场景是分页读大文件。比如跳过表头、每次取 100 行做批处理:

python 复制代码
from itertools import islice

def read_batches(file_path, batch_size=100):
    with open(file_path) as f:
        next(f)  # 跳过表头
        while True:
            # islice 从文件迭代器里切出 batch_size 行
            batch = list(islice(f, batch_size))
            if not batch:  # 切不出东西了,文件读完
                break
            yield batch

for batch in read_batches("huge.csv", batch_size=100):
    bulk_insert(batch)  # 每 100 行批量入库一次,内存恒定

注意 islice 不支持负索引(islice(it, -1) 会报错),因为它无法预知迭代器长度。需要「最后 N 个」时得换 collections.deque(it, maxlen=n)

组合起来:一条真正的处理流水线

三个函数串起来,就是一条不落地、内存恒定的数据管道。比如「合并多个已排序日志、按用户分组、只看前 3 个用户」:

python 复制代码
from itertools import chain, groupby, islice

def merged_sorted_logs(files):
    # 假设每个文件内部已按 user 排好序,这里简化为直接 chain
    # 生产环境跨文件全局有序应该用 heapq.merge
    lines = chain.from_iterable(open(f) for f in files)
    return (line.strip().split(",") for line in lines)

rows = merged_sorted_logs(["u1.log", "u2.log"])

# 按第一列(user)分组,只取前 3 组
for user, group in islice(groupby(rows, key=lambda r: r[0]), 3):
    print(user, sum(1 for _ in group))

整条链路里没有一次 list(全部数据),内存只跟「当前处理的一行 + 一组」有关。这就是 itertools 的价值:用惰性迭代把「空间换时间」翻转成「几乎不占空间」

小结

  • groupby 只合并相邻元素,不是全局分组------用前必须先按同样的 key 排序,否则同一 key 会被拆成多组。返回的 group 是一次性迭代器。
  • chain / chain.from_iterable 把多个数据源惰性接成一条流,合并多文件时内存恒定;动态数量的源用 from_iterable
  • islice 是迭代器版切片 ,能安全地从无限流或大文件里取前 N 个、做分页批处理;不支持负索引,要「最后 N 个」用 deque(maxlen=n)
  • 三者可自由串联,组成不落地的处理流水线。

一句话记忆:itertools 处理的是「流」不是「集合」,凡是能一行行流着处理的,就别整个读进内存

相关推荐
威联通网络存储2 小时前
TS-h2490FU在面板制造Array段AOI缺陷画廊中的并联
python·制造
接针2 小时前
UV 常用命令
python·uv
ziguo11224 小时前
深入浅出 C/C++ 数据类型:从入门到踩坑
linux·c语言·c++·windows·visual studio
无垠的广袤4 小时前
【工业树莓派 CM0 Dev Board】扩展板设计
linux·python·嵌入式硬件·pcb设计·模块化·传感器
会编程的土豆4 小时前
MySQL 入门:库、表、行、主键是什么
linux·数据库·网络协议·http
Yolanda_20224 小时前
在vscode终端中可使用pip,但是cmd终端中找不到命令 pip问题的解决
vscode·python·conda·pip
2301_764441335 小时前
用动力学系统(微分方程)为 Kernberg 的客体关系单元提供数学化的操作定义,把“自体—客体“这对心理结构建模成一个二维耦合系统
数据结构·python·算法·数学建模
dogstarhuang5 小时前
用 Doubao-Seed-Evolving + Python 免费写一个网页正文提取工具(实战教程)
爬虫·python·ai编程
阿扬ABCD5 小时前
ipmitool的Windows版本编译
windows·驱动开发