前言
先纠正题面里一个已经不成立的前提:reduce()在 Python 3 里不是内置函数。Python 3.0 的变更说明里写得很直接——「Removedreduce(). Usefunctools.reduce()if you really need it; however, 99 percent of the time an explicitforloop is more readable.」也就是说,reduce只在 Python 2 里是内置的;Python 2.7 已于 2020 年 1 月 1 日停止维护,今天写reduce(...)而不导入,会直接得到NameError: name 'reduce' is not defined。正确写法是from functools import reduce。
另一个误解是「reduce是 Python 里做聚合的正统方式」。官方文档自己都在劝退:对求和、求最大最小、求积这类需求,sum、max、math.prod更清楚也更快;reduce该留给真正需要自定义「从左到右累积」逻辑的场合。
本文讲清四件事:reduce的确切签名和语义、三个边界行为的处理、什么时候该用别的函数替代,以及几个确实值得用reduce的例子。示例以 Python 3 为基准;initial作为关键字参数需要 Python 3.14+,在此之前只能按位置传。
一、签名与语义
官方签名是:
functools.reduce(function, iterable, /[, initial])文档给出的近似实现是这样(下面这段是官方文档中的等价代码,可以直接跑):
# 摘自官方文档(适用于 Python 3.0+)
initial_missing = object()
def reduce(function, iterable, /, initial=initial_missing):
it = iter(iterable)
if initial is initial_missing:
value = next(it)
else:
value = initial
for element in it:
value = function(value, element)
return value语义可以概括为一句话:把iterable的第一个元素当作初始累积值(若提供了initial则用它),然后从左到右依次执行value = function(value, element),最终返回value。
# 适用于 Python 3.0+
from functools import reduce
print(reduce(lambda x, y: x + y, [1, 2, 3, 4, 5]))
# 15 —— 计算过程是 ((((1+2)+3)+4)+5)function的两个参数有明确分工:左边是累积值,右边是来自可迭代对象的新元素。这一点在非交换运算(减法、除法、字符串拼接、集合运算)里非常关键。
# 适用于 Python 3.0+
from functools import reduce
print(reduce(lambda x, y: x - y, [10, 1, 2])) # 7 —— ((10-1)-2)
print(reduce(lambda x, y: y - x, [10, 1, 2])) # 11 —— ((1-10) 之后再来一次)二、三个必须记住的边界行为
| 情况 | 行为 | 说明 |
|---|
iterable为空,未提供initial | 抛TypeError | 没有初始值可用 |
iterable为空,提供了initial | 返回initial | function一次都不调用 |
iterable只有一个元素,未提供initial | 直接返回该元素 | function一次都不调用 |
iterable只有一个元素,提供了initial | 返回function(initial, 元素) | 会调用一次 |
| 长可迭代对象 | 全程惰性迭代,不复制整个序列 | 可接受生成器 |
# 适用于 Python 3.0+
from functools import reduce
def add(x, y):
print(f" 调用 add({x}, {y})")
return x + y
print(reduce(add, [7])) # 没有任何输出,直接返回 7
print(reduce(add, [], 0)) # 没有任何输出,直接返回 0reduce的第一个参数是二元函数——只接受两个参数。写成三参数会报TypeError,提示这个函数需要 3 个参数但只收到了 2 个。
三、用reduce之前先看这张表
reduce能做的事里,绝大多数有更专用的替代品:
| 需求 | 推荐写法 | 备注 |
|---|
| 求和 | sum(iterable, start=0) | start可位置可关键字 |
| 求积 | math.prod(iterable, *, start=1) | 需要 Python 3.8+ |
| 求最大 / 最小 | max(iterable, *, key=..., default=...) | 空序列用default |
| 逻辑与 / 或 | all(iterable)/any(iterable) | 短路求值,遇到结果就停 |
| 字符串拼接 | "".join(parts) | 比反复x + y高效得多 |
| 保留每步中间结果 | itertools.accumulate(iterable, func) | 返回迭代器 |
| 字典合并 | {**a, **b} | 3.9+ 也可用字典合并运算符 |
| 展平一层嵌套 | 列表推导式 | 如[x for sub in data for x in sub] |
| 自定义累积逻辑 | functools.reduce | 真正该用它的时候 |
一句话原则:能用sum/max/any/join表达的,就不要用reduce。官方文档那句「99% 的情况显式for循环更可读」说的就是这个意思。
实战一:需要自定义累积时
reduce真正合适的地方,是聚合规则本身没有一个内置函数能表达。
# 适用于 Python 3.0+
from functools import reduce
import math
# 1) 求一组数的最大公约数
print(reduce(math.gcd, [12, 18, 24])) # 6
# 2) 求一组区间的交集
ranges = [{1, 2, 3, 4}, {2, 3, 4, 5}, {3, 4, 5, 6}]
print(reduce(lambda a, b: a & b, ranges)) # {3, 4}
# 3) 把多个字典合并,后者覆盖前者
dicts = [{"a": 1}, {"b": 2}, {"a": 9}]
print(reduce(lambda a, b: {**a, **b}, dicts)) # {'a': 9, 'b': 2}
# 4) 按顺序依次应用一批函数
funcs = [lambda s: s.strip(), lambda s: s.upper(), lambda s: s.replace(" ", "_")]
print(reduce(lambda acc, f: f(acc), funcs, " hello world ")) # HELLO_WORLD
# 5) 求「笛卡尔积式」的连乘
print(reduce(lambda a, b: a * b, [1, 2, 3, 4], 1)) # 24第 3 个例子要留意:{**a, **b}每次都会新建字典,元素多时开销明显;直接写for循环用一个字典原地update会更好。
第 4 个例子是reduce在函数式编程里的经典用法——把一串变换函数折叠成一次调用。它比嵌套调用f3(f2(f1(x)))更适合函数列表长度不固定的时候。
实战二:reduce与accumulate的区别
一个常见的混淆:想要「每一步的中间结果」,应该用itertools.accumulate,而不是在reduce里手动收集。
# 适用于 Python 3.0+
from functools import reduce
import itertools
nums = [1, 2, 3, 4, 5]
# reduce 只给最终值
print(reduce(lambda a, b: a + b, nums)) # 15
# accumulate 给每一步(默认 func 就是 operator.add)
print(list(itertools.accumulate(nums))) # [1, 3, 6, 10, 15]
# 也可以自己指定 func(位置参数)
import operator
print(list(itertools.accumulate(nums, operator.mul))) # [1, 2, 6, 24, 120]
# initial 是仅关键字参数,需要 Python 3.8+
print(list(itertools.accumulate(nums, initial=0))) # [0, 1, 3, 6, 10, 15]accumulate返回的是迭代器,initial是仅关键字参数,func是位置或关键字参数——这几点和reduce的initial处理方式不同,别记混。
常见坑点
1. 直接调用reduce而不导入
❌ 错误写法:
print(reduce(lambda x, y: x + y, [1, 2, 3]))
# NameError: name 'reduce' is not defined✅ 正确写法:
from functools import reduce
print(reduce(lambda x, y: x + y, [1, 2, 3])) # 6reduce在 Python 2 里是内置函数,Python 3.0 已把它移出内置命名空间。
2. 空序列没给initial
❌ 错误写法:
from functools import reduce
reduce(lambda x, y: x + y, []) # TypeError✅ 正确写法:
reduce(lambda x, y: x + y, [], 0) # 03. 把initial当成只能位置传的参数写反顺序
❌ 错误写法:
reduce(lambda x, y: x + y, 0, [1, 2]) # 参数顺序错了✅ 正确写法:顺序固定是「函数、可迭代对象、可选的 initial」:
reduce(lambda x, y: x + y, [1, 2], 0) # 3需要提醒的是,initial只有在 Python 3.14 起才能写成initial=0这种关键字形式;3.13 及更早的版本里它只能按位置传。
4. 用reduce做字符串拼接
❌ 错误写法:
from functools import reduce
parts = ["a", "b", "c"] * 10000
s = reduce(lambda x, y: x + y, parts) # 反复创建中间字符串✅ 正确写法:
s = "".join(parts)join只分配一次结果,reduce的x + y会生成大量用后即弃的中间字符串。
5. 误以为reduce会复制序列
❌ 错误想法:以为reduce是「先复制整个列表再操作」。
✅ 正确认识:reduce用迭代器逐个取元素,不复制序列,因此可以直接接收生成器:
from functools import reduce
print(reduce(lambda x, y: x + y, (i for i in range(5)), 0)) # 106. 交换了累积值与新元素的位置
❌ 错误写法(减法、除法、字符串拼接等非交换运算会算错):
from functools import reduce
print(reduce(lambda x, y: y - x, [10, 1, 2])) # 11,和直觉里的减法结果完全不同✅ 正确写法:左边是累积值,右边是新元素:
print(reduce(lambda x, y: x - y, [10, 1, 2])) # 77. 用可变对象做累积器,导致原地副作用
❌ 错误写法:
from functools import reduce
def collect(acc, x):
acc.append(x) # 原地修改,且返回值与 acc 是同一对象
return acc
print(reduce(collect, [1, 2, 3], [])) # [1, 2, 3]reduce里做原地修改本身能跑,但会掩盖「累积器被复用」的事实,一旦把初始值换成外部传进来的列表,就会污染调用方。这种情况用显式for循环表达得更清楚。
✅ 正确写法:要么纯函数式地返回新对象,要么直接用for:
result = []
for x in [1, 2, 3]:
result.append(x)8. 在reduce里嵌套lambda做函数复合,可读性崩掉
❌ 错误写法:
from functools import reduce
compose = reduce(lambda f, g: lambda x: f(g(x)), [f1, f2, f3])嵌套三层lambda之后,堆栈里全是<lambda>,出错时完全看不出是哪一步。
✅ 正确写法:给内层复合函数起个名字:
from functools import reduce
def compose_two(f, g):
def composed(x):
return f(g(x))
return composed
compose = reduce(compose_two, [f1, f2, f3])总结
| 项目 | 结论 |
|---|
| 模块 | functools.reduce,Python 3 里不是内置函数 |
| 签名 | functools.reduce(function, iterable, /[, initial]) |
| 参数顺序 | 左边累积值、右边新元素 |
| 空序列 | 无initial抛TypeError;有initial返回它 |
| 单元素 | 无initial时直接返回该元素,function不调用 |
initial关键字 | 需要 Python 3.14+,之前只能按位置传 |
| 首选替代 | sum/max/any/all/"".join/math.prod |
| 要中间结果 | 用itertools.accumulate,不是reduce |
reduce的定位很明确:当聚合规则没有一个内置函数能表达、且从左到右折叠的语义正好合适时才用它。记住三条硬事实——它住在functools里、function必须收两个参数、空序列必须先给initial;再用「求和求最值优先用内置函数」这条原则过滤一遍,绝大多数误用都能避免。