第9章 9.1 列表推导式与生成器表达式
🎯 开场 3 分钟:为什么要学这个?
上一章我们学会了用 pathlib 管理文件、用 typing 写出让机器人都能看懂的类型注解,手里已经有了不少趁手的工具。
但你有没有这种感觉:写代码时经常遇到这种情况——要处理一批数据,比如把 100 个文件名改成小写、过滤掉列表里的空值、从一堆数字里挑出偶数……
你的第一反应是不是写个 for 循环?没问题,能跑。但写出来的代码经常是:
result = []
for item in original_list:
if condition(item):
result.append(transform(item))
三行变五行,五行变十行。代码读起来像在绕圈子。
有没有更简洁的办法?
这一章我们要学的「列表推导式」和「生成器表达式」,就是来解决这个痛点的。学完之后,同样的功能你可能只需要一行代码,而且读起来更接近自然语言。
🧱 基础 25 分钟:核心概念
什么是列表推导式?说白了就是「一条流水线」
想象你去奶茶店点单。传统做法是这样:
老方法(for 循环):
# 场景:把 5 杯奶茶的价格都加上 10 元包装费
prices = [25, 30, 28, 35, 22]
new_prices = []
for price in prices:
new_prices.append(price + 10)
print(new_prices) # [35, 40, 38, 45, 32]
新方法(列表推导式):
prices = [25, 30, 28, 35, 22]
new_prices = [price + 10 for price in prices]
print(new_prices) # [35, 40, 38, 45, 32]
看明白了吗?[price + 10 for price in prices] 这一行,顶得上上面四行。
列表推导式的结构:
[ 对每个元素做的操作 for 变量 in 可迭代对象 ]
拆开来看:for price in prices 负责「从袋子里一个一个拿奶茶」,price + 10 负责「加上包装费」,中括号负责「把处理完的放回新袋子」。
带条件的列表推导式:流水线上的质检员
有时候我们不是处理所有元素,而是只处理符合某个条件的。这就像流水线上的质检员——只对合格品进行包装,不合格的直接跳过。
场景:只把偶数挑出来翻倍
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even_doubled = [n * 2 for n in numbers if n % 2 == 0]
print(even_doubled) # [4, 8, 12, 16, 20]
结构扩展:
[ 操作 for 变量 in 可迭代对象 if 条件 ]
多个条件也可以加:if n % 2 == 0 if n > 5 或者 if n % 2 == 0 and n > 5。
嵌套列表推导式:处理二维数据
想象你有个成绩单,每个学生有三门课成绩:
scores = [
[85, 90, 78], # 小明
[92, 88, 95], # 小红
[77, 82, 80], # 小刚
]
# 传统方法:先遍历每个学生,再遍历每门成绩
all_flat = []
for student in scores:
for score in student:
all_flat.append(score)
print(all_flat) # [85, 90, 78, 92, 88, 95, 77, 82, 80]
# 用嵌套列表推导式,一行搞定
all_flat = [score for student in scores for score in student]
print(all_flat) # [85, 90, 78, 92, 88, 95, 77, 82, 80]
注意看,嵌套推导式的 for 是从左到右写的,跟嵌套循环的顺序一致。

生成器表达式:自来水厂,按需供水
列表推导式虽好,但有个问题——它会一次性把所有结果都算出来存进内存。如果你的数据量是 1000 万条,那内存可能就爆了。
这时候我们需要「生成器表达式」,它就像自来水厂:不一次性生产完,而是你用多少,它生产多少。
核心区别:
# 列表推导式:一次性全部算出
list_result = [x * 2 for x in range(5)]
print(list_result) # [0, 2, 4, 6, 8]
print(type(list_result)) # <class 'list'>
# 生成器表达式:用圆括号,返回一个生成器对象
gen_result = (x * 2 for x in range(5))
print(gen_result) # <generator object <genexpr> at 0x...>
print(type(gen_result)) # <class 'generator'>
生成器对象像个水龙头,你拧一下(调用 next()),它就流出来一点:
gen = (x * 2 for x in range(5))
print(next(gen)) # 0
print(next(gen)) # 2
print(next(gen)) # 4
print(next(gen)) # 6
print(next(gen)) # 8
print(next(gen)) # StopIteration - 没水了!
什么时候用生成器?
- 数据量很大,不可能一次性装进内存
- 不需要重复访问,只是顺序遍历一次
- 配合
for循环使用时,语法几乎一样
# 用 for 循环遍历生成器,跟列表一样用
gen = (x * 2 for x in range(1000000))
for item in gen:
print(item) # 一个一个输出,不会撑爆内存
生成器表达式作为函数参数
生成器有一个骚操作:可以直接当成参数传给函数,不用加括号:
# 两种写法完全等价
sum(x * 2 for x in range(5)) # 不用括号包裹
sum((x * 2 for x in range(5))) # 用括号包裹
这在配合 sum()、max()、min()、any()、all() 这些内置函数使用时特别方便。
列表推导式 vs 生成器表达式 vs map/filter
老派 Python 程序员可能用过 map() 和 filter():
numbers = [1, 2, 3, 4, 5]
# map:把每个元素做操作
doubled = list(map(lambda x: x * 2, numbers))
# filter:过滤符合条件的元素
evens = list(filter(lambda x: x % 2 == 0, numbers))
说实话,这俩玩意儿阅读体验很差——lambda x: x * 2 这种匿名函数可读性远不如列表推导式 [x * 2 for x in numbers]。
我的建议:
- 简单操作 → 列表推导式
- 需要惰性计算 → 生成器表达式
- map/filter → 忘掉吧,你不需要它

🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):批量重命名文件
场景: 你下载了一批图片,名字乱七八糟,想把它们统一改成 photo_001.jpg、photo_002.jpg 这种格式。
import pathlib
# 模拟一堆乱名字的文件
files = ["IMG_1234.PNG", "screenshot2024.JPEG", "photo.PNG", "DSC08987.JPG", "pic1.png"]
# 用列表推导式统一改成小写 + 序号
renamed = [f"{name.rsplit('.', 1)[0].lower()}_{i:03d}.{name.rsplit('.', 1)[1].lower()}"
for i, name in enumerate(files, start=1)]
for original, new in zip(files, renamed):
print(f"{original} → {new}")
预期输出:
IMG_1234.PNG → img_1234_001.png
screenshot2024.JPEG → screenshot2024_002.jpeg
photo.PNG → photo_003.png
DSC08987.JPG → dsc08987_004.jpg
pic1.png → pic1_005.png
这一行的关键:enumerate(files, start=1) 给文件加上了序号,rsplit('.', 1)[1] 用来取文件扩展名。
项目 2(15 分钟):数据清洗小工具
场景: 你从 CSV 文件里读取了一批用户数据,里面有各种脏数据(空值、重复、格式不对)。你需要清洗它们。
# 模拟从CSV读取的数据(实际用 open('users.csv') 读取)
raw_data = [
{"name": "张三", "age": "25", "email": "zhangsan@example.com"},
{"name": "", "age": "30", "email": "lisi@example.com"}, # 名字为空
{"name": "王五", "age": "N/A", "email": "wangwu@example.com"}, # 年龄格式错
{"name": "赵六", "age": "28", "email": ""}, # 邮箱为空
{"name": "张三", "age": "25", "email": "zhangsan@example.com"}, # 重复数据
{"name": "孙七", "age": "35", "email": "sunqi@example.com"},
]
# 清洗规则:
# 1. 名字不能为空
# 2. 年龄必须是有效数字
# 3. 邮箱不能为空
# 4. 去除重复(保留第一次出现的)
def is_valid_age(age_str):
"""检查年龄是否是有效数字"""
try:
int(age_str)
return True
except (ValueError, TypeError):
return False
# 第一步:过滤掉无效数据
valid_data = [
row for row in raw_data
if row["name"] # 名字不为空
and is_valid_age(row["age"]) # 年龄有效
and row["email"] # 邮箱不为空
]
# 第二步:去除重复(根据邮箱判断)
seen_emails = set()
unique_data = [
row for row in valid_data
if row["email"] not in seen_emails
and not seen_emails.add(row["email"]) # add 返回 None,not None 恒为 True
]
# 第三步:格式化输出
cleaned = [
{
"name": row["name"],
"age": int(row["age"]),
"email": row["email"].lower()
}
for row in unique_data
]
print(f"原始数据:{len(raw_data)} 条")
print(f"清洗后:{len(cleaned)} 条")
print("\n清洗后的数据:")
for user in cleaned:
print(f" {user['name']} ({user['age']}岁) - {user['email']}")
预期输出:
原始数据:6 条
清洗后:4 条
清洗后的数据:
张三 (25岁) - zhangsan@example.com
王五 (35岁) - wangwu@example.com
孙七 (35岁) - sunqi@example.com
看明白了吗?三个列表推导式各司其职:第一个过滤脏数据,第二个去重,第三个做格式转换。
项目 3(15 分钟):日志分析小工具
场景: 你有一个访问日志文件,每行是一条记录,格式是 时间 IP 地址 状态码 URL。你想统计一下有哪些 404 错误(页面不存在)。
# 模拟日志数据(实际用 open('access.log') 读取)
log_lines = [
"2024-01-15 10:23:45 192.168.1.100 200 /index.html",
"2024-01-15 10:23:46 10.0.0.1 404 /missing.html",
"2024-01-15 10:23:47 192.168.1.101 200 /products.html",
"2024-01-15 10:23:48 172.16.0.5 500 /api/users",
"2024-01-15 10:23:49 192.168.1.100 404 /old-page.html",
"2024-01-15 10:23:50 10.0.0.2 404 /nonexistent.html",
"2024-01-15 10:23:51 192.168.1.102 200 /about.html",
]
# 解析日志行
def parse_log(line):
"""解析单行日志,返回 (时间, IP, 状态码, URL)"""
parts = line.split()
return parts[0] + " " + parts[1], parts[2], int(parts[3]), parts[4]
# 提取所有404记录
not_found = [
parse_log(line) for line in log_lines
if int(line.split()[3]) == 404
]
# 统计每个IP的404次数
from collections import Counter
ip_counts = Counter(record[1] for record in not_found)
# 生成报告
print("=" * 50)
print("404 错误统计报告")
print("=" * 50)
print(f"\n总共 {len(not_found)} 条 404 错误\n")
print("按 IP 地址统计:")
for ip, count in ip_counts.most_common():
print(f" {ip}: {count} 次")
print("\n详细信息:")
for time, ip, code, url in not_found:
print(f" [{time}] {ip} -> {url}")
预期输出:
==================================================
404 错误统计报告
==================================================
总共 3 条 404 错误
按 IP 地址统计:
10.0.0.1: 1 次
192.168.1.100: 1 次
10.0.0.2: 1 次
详细信息:
[2024-01-15 10:23:46] 10.0.0.1 -> /missing.html
[2024-01-15 10:23:49] 192.168.1.100 -> /old-page.html
[2024-01-15 10:23:50] 10.0.0.2 -> /nonexistent.html
这个工具虽然小,但已经具备了一个日志分析器的雏形。真实工作里,你可以把 log_lines 换成 open('access.log') 直接读文件。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:列表推导式 vs 生成器表达式,傻傻分不清
❌ 错误: 以为生成器表达式返回的是列表
result = (x * 2 for x in range(5))
print(result[0]) # TypeError: 'generator' object is not subscriptable
✅ 正确: 要么转成列表,要么直接用循环
result = (x * 2 for x in range(5))
result_list = list(result) # 一次性转成列表
print(result_list[0]) # 0
坑 2:在列表推导式里修改可变对象
❌ 错误: 列表推导式里不要用 append,那是给 for 循环用的
# 有人可能想这样写
result = [result.append(x) for x in range(5)] # 大错特错!
# result 变成了 [None, None, None, None, None]
✅ 正确: 列表推导式只做表达式计算,不做语句执行
result = [x for x in range(5)] # 这才是正确姿势
坑 3:生成器只能用一次
❌ 错误: 以为生成器可以反复遍历
gen = (x for x in range(3))
print(list(gen)) # [0, 1, 2]
print(list(gen)) # [] —— 第二次啥都没有了!
✅ 正确: 生成器是「一次性」的,用完就没了。要反复用就转成列表。
gen = (x for x in range(3))
gen_list = list(gen) # 先存起来
print(list(gen_list)) # [0, 1, 2]
print(list(gen_list)) # [0, 1, 2] —— 想用几次用几次
坑 4:字典推导式和集合推导式忘了加括号
❌ 错误: 把列表推导式的语法套到字典上
# 想生成 {0: 0, 1: 2, 2: 4}
result = {x: x*2 for x in range(3)} # 错误:花括号默认是 set!
print(result) # {0, 1, 2} —— 变成了集合,不是字典
✅ 正确: 字典推导式需要 key: value 格式
result = {x: x*2 for x in range(3)}
print(result) # {0: 0, 1: 2, 2: 4}
坑 5:列表推导式太长,写成一行鬼画符
❌ 错误: 为了炫技把代码全塞进一行
result = [func(x, y, z) for x in items if condition(x) for y in sub_items if condition2(x, y) for z in sub_sub_items if condition3(x, y, z)]
✅ 正确: 超过两个 for 嵌套就该拆成多行
result = [
func(x, y, z)
for x in items
if condition(x)
for y in sub_items
if condition2(x, y)
for z in sub_sub_items
if condition3(x, y, z)
]
性能小贴士:生成器真的能省内存
如果你要处理 100 万个数字的平方和,用列表推导式会很占内存:
import sys
# 列表版本
list_version = [x**2 for x in range(100000)]
print(f"列表占用: {sys.getsizeof(list_version)} bytes") # 约 800KB
# 生成器版本
gen_version = (x**2 for x in range(100000))
print(f"生成器占用: {sys.getsizeof(gen_version)} bytes") # 只有几百 bytes
差了上千倍!这就是生成器的威力。
调试技巧:把推导式拆成普通循环
如果你的列表推导式报错了,别硬着头皮调试,直接拆成普通 for 循环:
# 有 bug 的推导式
data = ["1", "2", "hello", "4"]
numbers = [int(x) for x in data] # 报错了,哪个出问题了?
# 拆开来定位
for x in data:
print(f"正在处理: {x}")
int(x) # 这行会报错,卡住的就是问题数据
✏️ 练习题 + 作业题
练习题(5 道,10 分钟内完成)
练习 1(2 分钟):抄改入门
- 输入:names = ["alice", "BOB", "Charlie"]
- 预期输出:['ALICE', 'BOB', 'CHARLIE']
- 提示:把项目 1 的代码复制过来,改一下具体变量
练习 2(2 分钟):加个条件
- 输入:numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
- 预期输出:只保留大于 5 的数字的平方 [36, 49, 64, 81, 100]
- 提示:在列表推导式最后加上 if n > 5
练习 3(3 分钟):处理新数据
- 输入:一组温度(摄氏温度)celsius = [0, 20, 37, 100]
- 预期输出:转成华氏温度 [32.0, 68.0, 98.6, 212.0]
- 提示:公式是 F = C * 9/5 + 32
练习 4(5 分钟):串联项目 2 和 3
- 输入:一组用户数据 ["Tom:25", "Jerry:invalid", "Spike:30", "":":"]
- 预期输出:过滤掉无效行后,转成字典 [{"name": "Tom", "age": 25}, {"name": "Spike", "age": 30}]
- 提示:先用 split(":") 拆开,过滤掉名字为空或年龄不是数字的,再用字典推导式转换格式
练习 5(5 分钟):读图分析
- 输入:以下代码运行后会报什么错?
gen = (x**2 for x in [1, 2, 3])
print(gen[0])
- 预期输出:说明错误类型和原因
- 提示:生成器对象能不能用下标访问?
作业题(1 道大题,30 分钟-2 小时)
作业:做个「数据汇总统计工具」
小明在一家小公司做运营,手上有一份销售数据(CSV 格式),想做个简单的统计分析。你帮他做一个 Python 脚本:
- 需求描述: 读取 CSV 文件,按月统计销售额,并找出销售额最高和最低的月份
- 功能点:
1. 用pathlib读取sales.csv文件(自己造一份模拟数据)
2. 用列表推导式/生成器表达式解析和过滤数据
3. 用字典推导式按月汇总
4. 找出最高/最低月份并输出 - 加分项:
1. 支持命令行参数指定文件路径
2. 输出漂亮的表格格式 - 验收标准: 能跑起来 + 输出正确 + 代码有中文注释
参考数据结构:
date,amount,product
2024-01-15,1200,笔记本电脑
2024-01-20,300,鼠标
2024-02-10,2500,显示器
2024-02-28,800,键盘
2024-03-05,1500,耳机
2024-03-20,200,鼠标垫
📚 总结 + 资源
本文学了 3 件事:
1. 列表推导式:[x for x in items] 让批量处理数据从 4 行变 1 行
2. 生成器表达式:(x for x in items) 用圆括号,按需生产,超级省内存
3. 实战组合拳:配合 pathlib 读文件、enumerate 加序号、collections 做统计
延伸学习资源:
- Python 官方文档 - 列表推导式
- 《Python Crash Course》第 5 章 - 列表操作
- 视频:B 站小甲鱼《零基础入门学 Python》第 38 讲
互动钩子:
你有没有遇到过这种情况——本来要写 10 行 for 循环,结果用列表推导式一行搞定?或者踩过生成器「只能用一次」的坑?评论区聊聊你是怎么解决的,老粉我优先回复!
下一章我们要学的「装饰器」,跟今天学的推导式是「好基友」——生成器返回的是数据流,装饰器能给你的函数加上一层「数据处理流水线」。准备好接收新工具吧!

评论(0)