Python itertools.accumulate 如何按分组重新开始累计
来源:17golang原创
时间:2026-10-13 09:35:20 233浏览 收藏
我在处理按批次到达的流水数据时,最容易误解的是:itertools.accumulate 能不能遇到分组标记就自动把累计值清零。答案是不能。它只会按照输入顺序,把当前累计值和下一个元素交给累计函数;要让每组重新开始,必须先识别边界,再为每组建立独立的累计过程。
Python 官方文档:https://docs.python.org/3/library/itertools.html#itertools.accumulate
最稳妥的做法是把“识别分组”和“组内累计”拆成两个阶段:先切分输入流,再对每个分组调用accumulate。如果数据已经按键连续排列,则用groupby逐组套用同一个累计函数。
先把问题边界说清楚
假设输入是一条带有分组标记的记录流,标记出现时代表下一条数据开启新批次。例如 None 表示边界,普通整数表示本组增量:
from itertools import accumulate # None 表示开启新组,数字表示当前组要累加的增量 stream = [1, 3, None, 2, 4, 1, None, 5] # accumulate 只会得到一条连续累计序列,不会识别 None 是重置标记 print(list(accumulate(value for value in stream if value is not None)))
这个例子会把所有数字拼成一条连续序列,得到的不是“每组从 1 开始”的结果。initial=0 也只能设置整条迭代器的初始输出,不能在中途反复触发重置,所以不要试图用一个全局 initial 解决分组边界。
完整工作流:先切分,再累计
第一步是把输入流转换为若干个连续分组。下面的生成器遇到边界标记就交出当前组,最后再补交一次未结束的组;这样既保留了输入顺序,也不会为了计算结果复制整条数据。
from collections.abc import Iterable, Iterator
from itertools import accumulate
def split_on_marker(stream: Iterable[int | None]) -> Iterator[list[int]]:
"""按 None 切分输入;空组不会被交出。"""
group: list[int] = []
for value in stream:
if value is None:
# 连续边界不产生空组,避免输出虚假的累计结果
if group:
yield group
group = []
continue
group.append(value)
# 输入不以边界结束时,仍要交出最后一组
if group:
yield group
stream = [1, 3, None, 2, 4, 1, None, 5]
result = [list(accumulate(group)) for group in split_on_marker(stream)]
print(result) # [[1, 4], [2, 6, 7], [5]]
这里的输出结构是“每组一个列表”。它适合需要明确边界、后续还要按组处理的任务;如果只想连续地产出带组号的结果,可以把内层列表改成生成器,并在外层逐项 yield。

大输入不要一次性保存所有分组
如果输入来自文件、消息队列或网络迭代器,通常不希望先构造完整的二维列表。可以让函数直接输出“组号、组内位置、累计值”,这样调用方消费一条就处理一条。
from collections.abc import Iterable, Iterator
from itertools import accumulate
def accumulated_by_marker(stream: Iterable[int | None]) -> Iterator[tuple[int, int]]:
"""惰性输出 (组号, 当前累计值),None 会开始下一组。"""
group_id = 0
total = 0
has_value = False
for value in stream:
if value is None:
# 只有已经读到数据时才推进组号,避免连续标记制造空组
if has_value:
group_id += 1
total = 0
has_value = False
continue
total += value
has_value = True
yield group_id, total
stream = [1, 3, None, 2, 4, 1, None, 5]
print(list(accumulated_by_marker(stream)))
# [(0, 1), (0, 4), (1, 2), (1, 6), (1, 7), (2, 5)]
这个版本没有直接调用 accumulate,但它表达了同一个“每组独立状态”的机制。若累计逻辑只是加法,显式状态机更容易把边界、组号和输出格式放在一起;若要复用任意二元函数,可以把 total += value 换成 total = function(total, value),并为第一项单独初始化。
数据已按连续键排列时使用 groupby
另一种常见输入是记录带有分组键,而且相同键已经连续出现。例如订单流按日期排序,同一天的金额记录挨在一起。这时可以用 itertools.groupby 得到每个连续分组,再把分组迭代器交给 accumulate。
from itertools import accumulate, groupby
records = [
("2026-10-01", 1),
("2026-10-01", 3),
("2026-10-02", 2),
("2026-10-02", 4),
]
result: list[tuple[str, int]] = []
for day, rows in groupby(records, key=lambda item: item[0]):
# 每个 rows 只属于一个连续键,累计器自然从第一条记录重新开始
totals = accumulate(amount for _, amount in rows)
result.extend((day, total) for total in totals)
print(result) # [('2026-10-01', 1), ('2026-10-01', 4), ('2026-10-02', 2), ('2026-10-02', 6)]
groupby 只把相邻且键相同的记录归为一组,不是 SQL 意义上“把全表同键记录搜集到一起”。如果同一个日期在输入中被拆成两段,它就会被视为两个分组;因此使用前要确认上游已经排序或已经按连续键组织好。

累计函数与边界条件怎么选
默认累计函数是加法,也可以传入接收两个参数的函数,例如运行最大值或自定义状态合并函数。无论采用哪种函数,都要先决定“新组的第一条记录”如何初始化:是直接作为初始累计值,还是从固定基线开始。
from itertools import accumulate from operator import mul # 乘法累计的初值来自每组第一项,不要额外插入 1,避免改变输出长度 group = [2, 3, 4] print(list(accumulate(group, mul))) # [2, 6, 24] # 如果确实需要固定初值,initial 会额外产生一项输出 print(list(accumulate(group, mul, initial=10))) # [10, 20, 60, 240]
实践中还要处理三个边界:开头就是分组标记时是否产生空组;连续标记是否只代表一个边界;输入以标记结束时是否丢弃最后的空组。本文的两个实现都选择“不输出空组”,如果业务需要保留空批次,应把这个规则写进输出协议,而不是依赖调用方猜测。
一张表选实现方案
| 输入形态 | 推荐方式 | 主要注意点 |
|---|---|---|
| 特殊值代表边界 | 先切分,再对每组 accumulate | 明确空组和末尾组规则 |
| 数据量大、需要流式输出 | 生成器维护组号和累计状态 | 第一条数据的初始化要单独处理 |
| 记录已按键连续排列 | groupby + accumulate | groupby 只识别相邻相同键 |
| 需要固定初始基线 | accumulate(..., initial=value) | 会比输入多输出一项 |
| 需要跨组保留总计 | 单独保存组外状态 | 不要把跨组状态混入组内累计器 |
小结
itertools.accumulate 解决的是“沿输入顺序不断合并”,不是“识别分组并重置”。遇到分组标记时,先用切分生成器明确边界;遇到已经按键连续排列的记录时,用 groupby 把每段交给独立的 accumulate。最后根据数据规模选择列表结果或惰性输出,并提前约定空组、末尾边界和初始值的语义。
-
346 收藏
-
235 收藏
-
275 收藏
-
234 收藏
-
497 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习