map/reduce
map() 和 reduce() 是函数式编程中处理数据的两个经典工具:
map():把一个函数应用到可迭代对象的每个元素,得到一串“转换后的值”;reduce():把一串值按顺序两两合并,最终得到一个值。
例如,把一组价格统一转换为含税价格是 map(),把这些价格累加成总价是 reduce()。它们都接收函数作为参数,因此也是高阶函数的典型例子。
本文使用 Python 3.10 及以上版本。读完后,你应该能够判断何时使用 map() 或 reduce(),并写出清晰、可调试的数据处理代码。
什么是高阶函数
如果一个函数满足下面任意条件,它就是高阶函数:
- 接收函数作为参数;
- 返回一个函数。
Python 中函数是对象,可以赋值、放进列表,也可以传给其他函数:
def double(number):
return number * 2
operation = double
print(operation(3)) # 6
传递函数时写函数名 double,表示“把函数对象传过去”;写 double(3) 则表示“现在调用函数并传递调用结果”。这个区别是使用 map() 的关键。
map():逐项转换
基本用法
map(function, iterable) 会对 iterable 中的每个元素调用 function:
def to_upper(text):
return text.upper()
words = ["python", "map", "function"]
result = map(to_upper, words)
print(list(result)) # ['PYTHON', 'MAP', 'FUNCTION']
map() 返回的是 map 迭代器,而不是列表。用 list() 可以一次性看到全部结果,也会把迭代器中的值全部取出。
使用 lambda 处理简单逻辑
只使用一次、且只有一个表达式的函数,可以用 lambda:
numbers = [1, 2, 3, 4]
squares = map(lambda number: number * number, numbers)
print(list(squares)) # [1, 4, 9, 16]
逻辑较长、需要复用或需要写文档时,优先使用有名称的 def。名称能让调试信息和代码阅读都更清楚。
map() 是惰性的
创建 map 对象时,转换函数还没有执行:
def show_and_double(number):
print(f"处理 {number}")
return number * 2
mapped = map(show_and_double, [1, 2, 3])
print("map 已创建")
print(next(mapped)) # 此时才输出“处理 1”,然后输出 2
迭代器会在消费时按需计算,因此适合处理较大的数据流。它只能顺序消费一次:
mapped = map(str, [1, 2, 3])
print(list(mapped)) # ['1', '2', '3']
print(list(mapped)) # [],迭代器已经耗尽
如果后续需要多次遍历,请保存为列表,或重新创建 map 对象。转换函数有副作用时,也要注意它只会在结果被消费时执行。
同时处理多个可迭代对象
map() 可以接收多个可迭代对象,函数也必须接收相同数量的参数:
left = [1, 2, 3]
right = [10, 20, 30]
totals = map(lambda first, second: first + second, left, right)
print(list(totals)) # [11, 22, 33]
迭代会在最短的可迭代对象结束时停止。长度不一致时如果必须及时发现错误,可以先检查长度,或使用 Python 3.10+ 的 zip(strict=True) 明确表达这一要求:
left = [1, 2, 3]
right = [10, 20]
try:
totals = [first + second for first, second in zip(left, right, strict=True)]
except ValueError:
print("输入数据长度不一致")
map() 与列表推导式
下面两种写法完成同样的转换:
numbers = [1, 2, 3]
mapped = list(map(lambda number: number * 2, numbers))
comprehension = [number * 2 for number in numbers]
一般可以这样选择:
- 只有转换逻辑时,列表推导式通常更直观;
- 已有一个命名函数,或需要把多个数据源按位置传给函数时,
map()很合适; - 结果很大、可以逐项消费时,保留
map迭代器可减少内存占用。
不要为了“函数式”而强行使用 map()。可读性和数据规模比形式更重要。
reduce():逐项合并
基本用法
reduce() 位于 functools 模块中,基本形式是 reduce(function, iterable[, initial])。函数每次接收两个值,并返回下一轮要使用的结果:
from functools import reduce
def add(total, number):
return total + number
numbers = [1, 2, 3, 4]
total = reduce(add, numbers)
print(total) # 10
计算过程相当于:
(((1 + 2) + 3) + 4) = 10
第一次调用是 add(1, 2),然后把结果 3 与下一个元素 3 合并,依次进行直到只剩一个累计结果。
使用初始值
传入第三个参数 initial,可以指定第一轮的累计值:
from functools import reduce
numbers = [1, 2, 3]
total = reduce(lambda total, number: total + number, numbers, 100)
print(total) # 106
它的过程是 (((100 + 1) + 2) + 3)。初始值有三个作用:
- 给空数据提供一个结果;
- 明确累计结果的类型;
- 表达业务上的起始值。
from functools import reduce
print(reduce(lambda total, number: total + number, [], 0)) # 0
print(reduce(lambda text, part: text + part, [], "")) # 空字符串
不传初始值时,空序列会抛出 TypeError;不传初始值时,非空序列的第一个元素会直接作为累计值,因此累计函数必须能处理这种类型。
非加法的合并操作
reduce() 不只用于求和,也可用于乘积、求最大值或合并字符串:
from functools import reduce
from operator import mul
factorial = reduce(mul, range(1, 6), 1)
print(factorial) # 120
不过,标准库已有更明确的工具时应优先使用它们:
from math import prod
print(prod(range(1, 6))) # 120
sum()、math.prod()、min() 和 max() 都比等价的 reduce() 更容易读懂,也通常经过更专门的实现优化。
累计函数必须满足的条件
传给 reduce() 的函数应当:
- 接收两个参数:当前累计值和下一个元素;
- 返回下一轮的累计值;
- 尽量保持类型稳定和无副作用。
例如,下面的函数错误地返回了 None,第二轮就无法继续计算:
from functools import reduce
def broken(total, number):
print(total + number)
# 没有 return,默认返回 None
# reduce(broken, [1, 2, 3]) # 第二轮会触发 TypeError
如果只是想输出每个元素,不要用 reduce() 模拟循环;直接使用 for 循环更清晰。
组合 map() 和 reduce()
数据处理通常可以拆成“先转换,再汇总”两步:
from functools import reduce
prices = [100, 200, 50]
tax_rate = 0.06
with_tax = map(lambda price: price * (1 + tax_rate), prices)
total = reduce(lambda subtotal, price: subtotal + price, with_tax, 0)
print(total) # 371.0
这里的 map() 先产生含税价格,reduce() 再逐项累加。两者都具有惰性,直到 reduce() 消费 with_tax 时,含税计算才逐项发生。
如果流程中还要筛选数据,可以加入 filter();但当表达式变长时,建议拆成命名函数,或者使用普通循环让中间状态更明显:
from functools import reduce
def is_valid_price(price):
return price >= 0
def add(total, price):
return total + price
prices = [100, -20, 50]
total = reduce(add, map(float, filter(is_valid_price, prices)), 0.0)
print(total) # 150.0
类型提示与函数签名
类型提示能帮助读者和编辑器理解高阶函数的接口。collections.abc.Callable 可表示一个可调用对象:
from collections.abc import Callable, Iterable, Iterator
def transform(values: Iterable[int], operation: Callable[[int], str]) -> Iterator[str]:
return map(operation, values)
result = transform([1, 2, 3], str)
print(list(result)) # ['1', '2', '3']
这里的 Callable[[int], str] 表示函数接收一个 int,返回一个 str。Iterable 比 list 更灵活,调用方可以传入元组、生成器或文件对象;返回 Iterator 则提醒调用者结果是惰性的、只能顺序消费。
常见错误与排查方法
提前调用回调函数
numbers = [1, 2, 3]
# 错误:先执行 double(数字) 得到一个值,再把值传给 map
# map(double(2), numbers)
# 正确:传入函数对象
map(double, numbers)
忘记物化结果
调试时直接 print(map(...)) 只能看到迭代器对象的信息。需要查看结果时使用 list(),但要记住这会消耗迭代器并占用与结果数量相关的内存。
复用已经耗尽的迭代器
mapped = map(str, [1, 2, 3])
print(list(mapped))
print(list(mapped)) # []
需要两次结果时,可以写成 values = list(map(...)),或为每次使用重新创建 map()。
用 reduce() 代替所有循环
复杂的累计逻辑、需要提前退出、需要处理异常或需要多个中间变量时,reduce() 往往会降低可读性。优先考虑 sum()、any()、all()、min()、max()、math.prod() 或普通 for 循环。
忽略空输入和类型
对可能为空的输入,应提供合适的初始值;同时确认累计函数不会在第一轮遇到不兼容的类型:
from functools import reduce
values = []
total = reduce(lambda left, right: left + right, values, 0)
print(total) # 0
选择建议
可以用下面的规则快速判断:
| 需求 | 推荐写法 |
|---|---|
| 对每个元素做同一种转换 | map() 或列表推导式 |
| 把所有元素加总 | sum() |
| 把所有元素相乘 | math.prod() |
| 判断是否至少一个元素满足条件 | any() |
| 判断是否所有元素满足条件 | all() |
| 需要自定义的顺序累计 | reduce() |
| 需要复杂分支、提前退出或多个状态 | for 循环 |
练习
- 使用
map()把字符串列表['1', '2', '3']转换为整数列表。 - 使用
map()和两个可迭代对象,计算两组成绩的平均分。 - 使用
reduce()计算列表中所有数字的乘积,并处理空列表。 - 先过滤出订单金额大于 100 的项目,再使用
map()计算折扣价,最后用sum()求总价。 - 把一个过长的
lambda改写成带有类型提示和文档字符串的命名函数。
小结
- 高阶函数可以接收函数或返回函数;Python 的函数对象可以像普通值一样传递。
map()对每个元素执行转换,返回惰性迭代器;多个输入会按位置传给回调函数。reduce()从左到右把元素合并为一个结果,来自functools;初始值可以处理空输入并明确累计类型。- 惰性结果只有在迭代时才计算,消费后不能自动重置;需要复用时应物化或重新创建。
- 业务代码优先选择语义明确的
sum()、math.prod()等工具;reduce()只用于确实需要自定义累计规则的场景。 - 当函数式写法变得难读时,命名函数或普通循环通常是更好的教学和维护选择。