跳到主要内容

map/reduce

map() 和 reduce() 是函数式编程中处理数据的两个经典工具:

  • map():把一个函数应用到可迭代对象的每个元素,得到一串“转换后的值”;
  • reduce():把一串值按顺序两两合并,最终得到一个值。

例如,把一组价格统一转换为含税价格是 map(),把这些价格累加成总价是 reduce()。它们都接收函数作为参数,因此也是高阶函数的典型例子。

本文使用 Python 3.10 及以上版本。读完后,你应该能够判断何时使用 map() 或 reduce(),并写出清晰、可调试的数据处理代码。

什么是高阶函数​

如果一个函数满足下面任意条件,它就是高阶函数:

  1. 接收函数作为参数;
  2. 返回一个函数。

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 循环

练习​

  1. 使用 map() 把字符串列表 ['1', '2', '3'] 转换为整数列表。
  2. 使用 map() 和两个可迭代对象,计算两组成绩的平均分。
  3. 使用 reduce() 计算列表中所有数字的乘积,并处理空列表。
  4. 先过滤出订单金额大于 100 的项目,再使用 map() 计算折扣价,最后用 sum() 求总价。
  5. 把一个过长的 lambda 改写成带有类型提示和文档字符串的命名函数。

小结​

  • 高阶函数可以接收函数或返回函数;Python 的函数对象可以像普通值一样传递。
  • map() 对每个元素执行转换,返回惰性迭代器;多个输入会按位置传给回调函数。
  • reduce() 从左到右把元素合并为一个结果,来自 functools;初始值可以处理空输入并明确累计类型。
  • 惰性结果只有在迭代时才计算,消费后不能自动重置;需要复用时应物化或重新创建。
  • 业务代码优先选择语义明确的 sum()、math.prod() 等工具;reduce() 只用于确实需要自定义累计规则的场景。
  • 当函数式写法变得难读时,命名函数或普通循环通常是更好的教学和维护选择。