知识点思维导图
21 个知识节点
参考资料
Python(06) - 列表推导式与生成器
读完后,你应能完成以下任务:
- 绘制“Python(06) - 列表推导式与生成器 / 先建立前端锚点”的关键对象与数据流,解释“核心切入点:列表推导式 ≈ map/filter 的合体语法糖,生成器 ≈ JS 的 function 一对一映射。”,并用源码位置、日志或 Trace 标注证据。
- 为“Python(06) - 列表推导式与生成器 / 列表推导式:map + filter 的合体”设计正常与异常输入,验证“读法对照:把 nums.map(x => x 2) 拆成三段——x 2(要算什么)、x(每个元素叫什么)、nums(从哪来),重新排成 [x 2 for x in nums] 就是 Python 写法。”,输出首个偏差位置与回归测试结果。
- 实现“Python(06) - 列表推导式与生成器 / map + filter 一次写完”的最小代码或配置,检验“注意顺序:Python 推导式里 for...if 在后、要算的表达式在前,和阅读习惯相反,但写多了就顺手了。”,输出命令、结果与 Diff,并说明不适用边界。
你在 JS 里写惯了
arr.map().filter(),到了 Python 会发现大家很少这么链式写,而是用一种叫"列表推导式"的语法糖。本篇解决两个问题:怎么用比map/filter更地道的方式处理集合;以及当数据量大到不想一次性装进内存时,怎么用yield做"惰性流"。后者直接对标你见过的function*。
一、先建立前端锚点
| 你在 JS 里这么做 | Python 地道写法 | 类别 |
|---|---|---|
arr.map(x => x * 2) |
[x * 2 for x in arr] |
列表推导式 |
arr.filter(x => x > 0) |
[x for x in arr if x > 0] |
带条件的推导式 |
function* gen() { yield 1 } |
def gen(): yield 1 |
生成器 |
for (const v of iterable) |
for v in iterable |
迭代 |
核心切入点:列表推导式 ≈ map/filter 的合体语法糖,生成器 ≈ JS 的 function* 一对一映射。下面分别讲透。
二、列表推导式:map + filter 的合体
2.1 最基础:等价于 map
并排看 JS:
const nums = [1, 2, 3, 4]
const doubled = nums.map(x => x * 2) // [2, 4, 6, 8]
读法对照:把 nums.map(x => x * 2) 拆成三段——x * 2(要算什么)、x(每个元素叫什么)、nums(从哪来),重新排成 [x * 2 for x in nums] 就是 Python 写法。
2.2 加条件:等价于 filter
const nums = [1, 2, 3, 4, 5, 6]
const evens = nums.filter(x => x % 2 === 0) // [2, 4, 6]
2.3 map + filter 一次写完
JS 里 map 和 filter 要链两次,Python 一个推导式搞定:
const nums = [1, 2, 3, 4, 5, 6]
const result = nums.filter(x => x % 2 === 0).map(x => x * 10) // [20, 40, 60]
注意顺序:Python 推导式里 for...if 在后、要算的表达式在前,和阅读习惯相反,但写多了就顺手了。
2.4 字典推导式 / 集合推导式
把方括号换成花括号,就能直接生成 dict 或 set:
JS 没有等价语法糖,得手动 reduce 或 Object.fromEntries:
const names = ["tom", "jerry"]
const nameLen = Object.fromEntries(names.map(n => [n, n.length])) // {tom:3, jerry:5}
三、边界:哪里和 JS 不一样
类比建立了直觉,但必须立刻划清差异,别过度套用 JS 心智:
-
没有
[].map的链式语法主流地位。Python 也有map()/filter()内置函数,但它们返回的是"惰性迭代器"而非列表,且社区公认推导式更可读。能用推导式就别用map/filter。 -
推导式里的变量不泄漏到外层(Python 3 起)。下面的
x不会污染外部作用域,这点和 JS 的let块级作用域一致,但和 Python 普通for循环不同——普通for的循环变量会留在外面。 -
别为了炫技写嵌套三层推导式。可读性崩了就老老实实用普通
for。推导式适合"一行能说清"的转换。 -
range(n)不是数组,是惰性的范围对象。[i for i in range(3)]才得到列表[0,1,2]。它类似 JS 里没有的"惰性 0..n 序列"。
四、生成器:yield 一对一对标 function
4.1 心智模型直接搬 JS
你在 JS 里见过这个吗?
function* countUp() {
yield 1 // 暂停在这,把 1 交出去
yield 2
yield 3
}
const it = countUp()
it.next().value // 1,每次 next 才往下走一步
Python 几乎逐字对应,只是去掉了 *,函数体里出现 yield 它就自动成为生成器:
对照表:
| JS | Python |
|---|---|
function* g() {} |
def g(): ...(函数内含 yield) |
yield v |
yield v |
it.next().value |
next(it) |
for (const v of g()) |
for v in g() |
迭代结束 done: true |
抛出 StopIteration(for 会自动处理) |
4.2 为什么要用它:惰性 + 省内存
普通函数是"一次性把结果全做完返回",生成器是"要一个给一个"。处理大数据时差别巨大:
如果用列表,["..." for line in ...] 会把几百万行全装进内存;生成器则是流式的,内存占用恒定。这正是 function* 在 JS 里处理无限/超大序列的同款理由。
4.3 生成器表达式:推导式的惰性版
把列表推导式的方括号 [] 换成圆括号 (),就从"立刻算出整个列表"变成"惰性生成器":
经验法则:只是要遍历/聚合一次就用生成器表达式 ();需要反复访问、索引、求长度才用列表 []。
五、最容易踩的坑
-
生成器只能消费一次。遍历完就空了,不像列表能反复 for。这是新手最常见的崩溃点。
对照 JS 的 generator 也是同样行为——迭代器一旦走完就
done。需要复用就转成列表存起来。 -
想要列表却写成了生成器。
(x for x in nums)不是元组,是生成器;print它会看到<generator object ...>而不是内容。要列表请用[],或外面包list(...)。 -
推导式里调用有副作用的函数。推导式应当是"纯转换"。如果你在里面
print或改外部状态,说明该用普通for。 -
超大数据还用列表推导式。这会瞬间吃满内存。判断标准:源数据大 + 只遍历一次 → 用生成器表达式
()或yield。
六、综合示例:读日志统计错误
把本篇知识串起来,写一个流式统计:
七、总结
- 列表推导式:map + filter 的合体:读法对照:把 nums.map(x => x 2) 拆成三段——x 2(要算什么)、x(每个元素叫什么)、nums(从哪来),重新排成 [x 2 for x in nums] 就是 Python 写法。
- 边界:哪里和 JS 不一样:没有 [].map 的链式语法主流地位。 -> 推导式里的变量不泄漏到外层(Python 3 起)。 -> 别为了炫技写嵌套三层推导式。 -> range(n) 不是数组,是惰性的范围对象。
- 生成器:yield 一对一对标 function:普通函数是"一次性把结果全做完返回",生成器是"要一个给一个"。
- 最容易踩的坑:生成器只能消费一次。遍历完就空了,不像列表能反复 for。这是新手最常见的崩溃点。 -> 想要列表却写成了生成器。(x for x in nums) 不是元组,是生成器;print 它会看到 而不是内容。要列表请用 [],或外面包 list(...)。 -> 推导式里调用有副作用的函数。 -> 超大数据还用列表推导式。这会瞬间吃满内存。判断标准:源数据大 + 只遍历一次 → 用生成器表达式 () 或 yield。
- 生成器表达式:推导式的惰性版:经验法则:只是要遍历/聚合一次就用生成器表达式 ();
学完自测
选择所有正确答案;提交后逐项核对判断依据。