第3章 3.1 数组高阶方法 map/filter/reduce
🎯 开场:为什么你需要一个"数据加工流水线"?
你有没有遇到过这种情况——
手里有一份名单,需要把每个人的名字后面加个"同学",再过滤掉姓"王"的,最后统计还剩多少人。一顿操作下来,代码写了 30 行,自己都看晕了。
或者这样——从 API 拿到一批数据,要转换成自己需要的格式。用 for 循环写了半天,改需求时发现逻辑全缠在一起了。
这就是为什么我们要学 map/filter/reduce。
它们三个是处理数据的"三剑客",可以把复杂的数据处理流程,变成一条清晰可读的流水线。学完这一章,你再遇到类似场景,10 行代码就能搞定。
上一章我们学会了循环和条件判断,这一章我们要把"批量处理数据"这件事,做得更优雅、更高效。
🧱 基础:三个概念,用点菜来理解
map:流水线上的"每件都加工"
生活类比:想象你在火锅店点菜,每份菜上来前都要经过"去冰、洗净、切块"三步。这个"每份菜都要走一遍"的流程,就是 map。
解决什么\n\n
\n\n
\n\n:对数组里每一个元素做同样的转换。
怎么用:
# 原始数据:一堆数字
prices = [30, 50, 20, 80]
# 用 map 给每个价格打八折
discounted = list(map(lambda x: x * 0.8, prices))
print(discounted) # [24.0, 40.0, 16.0, 64.0]
这行代码在说:把 prices 里的每个数,乘以 0.8。
filter:流水线上的"筛选闸门"
生活类比:机场安检,每个行李都要过一遍扫描。能过的过,不能过的被拦下。这个"符合条件的留下,不符合的踢掉"的操作,就是 filter。
解决什么:从数组里挑出满足条件的元素。
怎么用:
# 原始数据:一堆数字
scores = [45, 78, 92, 33, 88, 55]
# 用 filter 挑出及格的成绩(>= 60)
passed = list(filter(lambda x: x >= 60, scores))
print(passed) # [78, 92, 88]
这行代码在说:从 scores 里,只留下大于等于 60 的数。
reduce:流水线上的"汇总站"
生活类比:工厂流水线,每个零件依次经过,最终变成一辆整车。reduce 就是那个"把所有东西攒成一个结果"的操作。
解决什么:把数组的所有元素合并成一个值(求和、求积、拼接字符串等)。
怎么用:
from functools import reduce
# 原始数据:一堆数字
sales = [120, 350, 80, 200]
# 用 reduce 求总和
total = reduce(lambda a, b: a + b, sales)
print(total) # 750
这行代码在说:把 sales 里的数,从左到右,一个一个加起来。
它们仨配合起来长这样
from functools import reduce
data = [30, 50, 20, 80, 45]
# 场景:所有价格打九折 → 过滤掉小于40的 → 求总和
result = reduce(
lambda a, b: a + b,
filter(lambda x: x >= 40, map(lambda x: x * 0.9, data))
)
print(result) # 175.5
拆解一下:
1. map(lambda x: x * 0.9, data) → 每个价格乘 0.9
2. filter(lambda x: x >= 40, ...) → 只留大于等于 40 的
3. reduce(lambda a, b: a + b, ...) → 把剩下的加起来
🔥 实战:三个项目,从入门到能用
项目 1:成绩单处理器(5 分钟)
场景:班主任有一份成绩单,要转换成"及格/不及格"的标签。
# 小明的考试成绩
scores = [85, 42, 77, 93, 56, 61, 38, 89]
# 用 map 把分数转成及格/不及格标签
labels = list(map(lambda s: "及格" if s >= 60 else "不及格", scores))
print("成绩单:")
for score, label in zip(scores, labels):
print(f" {score}分 → {label}")
# 用 filter 统计及格人数
passed = list(filter(lambda s: s >= 60, scores))
print(f"\n及格人数:{len(passed)}")
# 用 reduce 算平均分
average = reduce(lambda a, b: a + b, scores) / len(scores)
print(f"平均分:{average:.1f}")
预期输出:
成绩单:
85分 → 及格
42分 → 不及格
77分 → 及格
...
及格人数:5
平均分:67.6
一句话解释:map 转换、filter 筛选、reduce 汇总,三剑客各司其职。
项目 2:电商数据清洗(15 分钟)
场景:从 CSV 文件读入一批商品数据,筛选出打折商品,计算折扣后的总价。
from functools import reduce
# 模拟从 CSV 读到的数据
products = [
{"name": "iPhone", "price": 6999, "discount": 0},
{"name": "AirPods", "price": 1299, "discount": 0.15},
{"name": "MacBook", "price": 9999, "discount": 0},
{"name": "iPad", "price": 3299, "discount": 0.2},
{"name": "Apple Watch", "price": 2199, "discount": 0.1},
]
# 需求1:找出所有打折商品
onsale = list(filter(lambda p: p["discount"] > 0, products))
print("打折商品:")
for p in onsale:
final_price = p["price"] * (1 - p["discount"])
print(f" {p['name']}:原价{p['price']},折后价{final_price:.0f}")
# 需求2:计算所有打折商品的折扣后总价
total = reduce(
lambda acc, p: acc + p["price"] * (1 - p["discount"]),
onsale,
0 # 初始值设为0
)
print(f"\n打折商品总价(折扣后):{total:.2f}元")
# 需求3:找出折扣力度最大的商品
best_deal = reduce(
lambda best, p: p if p["discount"] > best["discount"] else best,
onsale
)
print(f"折扣力度最大:{best_deal['name']}({best_deal['discount']*100:.0f}% off)")
预期输出:
打折商品:
AirPods:原价1299,折后价1104
iPad:原价3299,折后价2639
Apple Watch:原价2199,折后价1979
打折商品总价(折扣后):5722.00元
折扣力度最大:iPad(20% off)
一句话解释:先 filter 挑出打折的,再用 reduce 累计求和/比较,一条链搞定。
项目 3:个人待办清单工具(15 分钟)
场景:做一个命令行待办清单,支持添加任务、按优先级筛选、统计待办数量。
from functools import reduce
# 模拟已有的待办数据
todos = [
{"id": 1, "task": "写周报", "priority": 2, "done": False},
{"id": 2, "task": "开会", "priority": 3, "done": True},
{"id": 3, "task": "写代码", "priority": 1, "done": False},
{"id": 4, "task": "回邮件", "priority": 2, "done": False},
{"id": 5, "task": "健身", "priority": 3, "done": False},
]
def show_menu():
print("\n===== 待办清单 =====")
print("1. 查看所有待办")
print("2. 只看高优先级")
print("3. 统计待办数量")
print("4. 标记完成")
print("5. 退出")
def show_all_todos():
items = list(map(
lambda t: f"[{'√' if t['done'] else ' '}] {t['task']} (优先级:{t['priority']})",
todos
))
print("\n".join(items))
def show_high_priority():
high = list(filter(lambda t: t["priority"] >= 3 and not t["done"], todos))
if high:
print("高优先级待办:")
for t in high:
print(f" ★ {t['task']}")
else:
print("没有未完成的高优先级任务!")
def count_pending():
pending = list(filter(lambda t: not t["done"], todos))
print(f"还有 {len(pending)} 项待办")
def mark_done(todo_id):
global todos
todos = list(map(
lambda t: {**t, "done": True} if t["id"] == todo_id else t,
todos
))
print(f"任务 {todo_id} 已标记完成")
# 简单交互
print("欢迎使用待办清单!")
show_menu()
# 演示几个操作
print("\n--- 查看所有 ---")
show_all_todos()
print("\n--- 高优先级筛选 ---")
show_high_priority()
print("\n--- 统计待办 ---")
count_pending()
print("\n--- 标记完成 ---")
mark_done(1)
show_all_todos()
预期输出:
欢迎使用待办清单!
===== 待办清单 =====
...
--- 查看所有 ---
[ ] 写周报 (优先级:2)
[√] 开会 (优先级:3)
[ ] 写代码 (优先级:1)
...
--- 高优先级筛选 ---
高优先级待办:
开会
健身
--- 统计待办 ---
还有 4 项待办
--- 标记完成 ---
[√] 写周报 (优先级:2)
...
一句话解释:map 用来格式化显示,filter 用来按条件筛选,reduce 可以做统计汇总。
💪 进阶:5 个坑 + 1 个技巧
坑 1:忘了转 list
# ❌ 错误:map 返回的是迭代器,不是列表
result = map(lambda x: x * 2, [1, 2, 3])
print(result[0]) # TypeError: 'map' object is not subscriptable
# ✅ 正确:用 list() 包一下
result = list(map(lambda x: x * 2, [1, 2, 3]))
print(result[0]) # 2
坑 2:reduce 忘记初始值
# ❌ 错误:空数组没有初始值会报错
from functools import reduce
result = reduce(lambda a, b: a + b, []) # TypeError: reduce() of empty sequence
# ✅ 正确:给个初始值
result = reduce(lambda a, b: a + b, [], 0) # 0
坑 3:map/filter 里面写复杂逻辑
# ❌ 错误:lambda 写太长太复杂,可读性差
data = [1, 2, 3, 4, 5]
result = list(map(lambda x: (x * 2 if x > 2 else x + 1) ** 2, data))
# ✅ 正确:拆成多步,或者用 def 定义清楚
def transform(x):
if x > 2:
return x * 2
return x + 1
result = list(map(lambda x: transform(x) ** 2, data))
坑 4:filter 后想取第一个但没考虑空情况
# ❌ 错误:filter 返回空列表时取 [0] 会报错
nums = [1, 2, 3]
first_even = list(filter(lambda x: x > 100, nums))[0] # IndexError
# ✅ 正确:加个判断,或者用 next() 加默认值
first_even = next(filter(lambda x: x > 100, nums), None)
if first_even is None:
print("没找到")
坑 5:在 map 里修改外部变量
# ❌ 错误:闭包陷阱,以为能修改变量
count = 0
result = list(map(lambda x: (count := count + 1, x)[1], [1, 2, 3]))
# 这能跑,但容易出错,而且老手看了会皱眉头
# ✅ 正确:用 enumerate 或者别的办法
result = [(i, x) for i, x in enumerate([1, 2, 3], 1)]
性能小贴士:链式调用 vs 列表推导式
# 数据量小时,链式调用可读性好
result = list(filter(lambda x: x > 0, map(lambda x: x * 2, range(100))))
# 数据量大时,列表推导式更快(因为没有 lambda 函数调用开销)
result = [x * 2 for x in range(100) if x > 0]
调试技巧:加点日志
# 在 lambda 里加打印(临时调试用)
data = [1, 2, 3, 4, 5]
result = list(map(lambda x: (print(f"处理: {x}"), x * 2)[1], data))
# 打印所有中间过程
# 或者用 reduce 时每步打印
from functools import reduce
result = reduce(
lambda acc, x: (print(f"{acc} + {x} = {acc + x}"), acc + x),
[1, 2, 3, 4, 5]
)
✏️ 练习题
练习 1(2 分钟):换个数据
- 输入:[10, 20, 30, 40]
- 预期输出:[20, 40, 60, 80](每个数翻倍)
- 提示:把项目 1 的 lambda 里的计算改一下
练习 2(3 分钟):换个条件
- 输入:[3, 7, 2, 9, 4]
- 预期输出:[7, 9](只留大于 5 的)
- 提示:filter 的条件从 x >= 60 改成 x > 5
练习 3(10 分钟):统计字数
- 输入:["苹果", "香蕉", "苹果", "橙子", "香蕉", "苹果"]
- 预期输出:{"苹果": 3, "香蕉": 2, "橙子": 1}
- 提示:用 reduce,每次遇到水果就往字典里计数
练习 4(15 分钟):组合拳
- 输入:[{"name": "A", "score": 85}, {"name": "B", "score": 55}, {"name": "C", "score": 72}]
- 预期输出:平均分 70.67,及格名单 ["A", "C"]
- 提示:先 filter 再 map 再 reduce
练习 5(5 分钟):报错分析
- 输入:以下代码运行后报什么错?
from functools import reduce
print(reduce(lambda a, b: a + b, [1, 2, 3]))
- 预期输出:能正常运行,输出
6 - 提示:这道题其实没坑,注意 reduce 不需要初始值也能处理非空数组
作业:做一个「数据报表生成器」
做一个命令行工具,读取以下模拟数据,生成一份简单的销售报表:
sales = [
{"region": "华北", "product": "手机", "amount": 15000, "cost": 12000},
{"region": "华北", "product": "电脑", "amount": 28000, "cost": 22000},
{"region": "华东", "product": "手机", "amount": 22000, "cost": 18000},
{"region": "华东", "product": "平板", "amount": 12000, "cost": 9000},
{"region": "华南", "product": "手机", "amount": 18000, "cost": 14000},
]
功能点:
1. 计算每个区域的利润(amount - cost)
2. 找出利润最高的区域
3. 按产品类型统计总销售额
加分项:
1. 支持按区域筛选
2. 输出带点装饰的格式化报表
验收标准:能跑起来 + 输出清晰易读 + 代码有注释
📚 总结
本文学了 3 个核心点:
- map:对每个元素做同样的转换
- filter:按条件筛选元素
- reduce:把元素合并成一个结果
延伸学习资源:
- 官方文档:functools 模块文档(reduce 在这里)
- 进阶阅读:《流畅的 Python》第 5 章,高阶函数部分
- 视频推荐:B 站「小甲鱼」Python 教程函数式编程那一集
你在处理数据时,更喜欢用 map/filter/reduce 还是列表推导式?评论区聊聊,老粉优先回复!
📌 下一章我们要聊一个让很多人困惑的话题——「对象与原型链」。你有没有想过,一个对象是怎么知道它有哪些方法的?
__proto__和prototype到底有什么区别?剧透一点:下章会用到这一章的 map/filter 思路来处理对象数据。

评论(0)