知识点思维导图
25 个知识节点
参考资料
Python(35) - A1 - 附录-Python陷阱对照
你在 JS 里被
var提升、==隐式转换、对象引用复制坑过——Python 也有一组"长得人畜无害、踩下去很疼"的坑。本篇专治前端转 Python 最高频的四类翻车:可变默认参数、循环里的闭包、is与==混用、深浅拷贝。每个坑都给出 JS 对照,告诉你哪里"和 JS 一样别慌"、哪里"和 JS 不一样要改心智"。
一、切入点:你早就被语言坑过
JS 有它的"经典面试题坑":for (var i...) 循环里 setTimeout 全打印同一个值、[] == ![] 为 true、const obj2 = obj1 改一个动另一个。Python 的坑结构上一一对应,只是触发点换了位置。把这篇当成"Python 版的 JS 怪异题集"来读最省力。
| JS 的经典坑 | Python 的对应坑 | 本篇章节 |
|---|---|---|
for (var i...) + 异步闭包打印同值 |
循环里 lambda/闭包共享变量 | 二 |
== 隐式类型转换乱套 |
is 当成 == 用 |
三 |
obj2 = obj1 引用复制改一动二 |
列表/字典赋值与浅拷贝 | 四 |
| 函数参数对象被意外共享 | 可变默认参数只创建一次 | 一 |
二、可变默认参数:函数的"默认值"只生成一次
这是 Python 独有、JS 没有对应物的头号坑,新手几乎必踩。
为什么会这样(WHY):Python 的默认参数值在函数定义时只计算一次,之后所有调用共享同一个对象。items=[] 这个空列表在 def 执行时就被创建好、绑死在函数上,不是每次调用都新建。
JS 程序员的直觉来自这里——JS 的默认参数是每次调用都重新求值的:
// JS:默认值每次调用都重新生成一个新数组,所以没有这个坑
function append(item, items = []) {
items.push(item)
return items
}
append(1) // [1]
append(2) // [2] —— 干净,互不影响
正确写法:默认值用 None(不可变哨兵),在函数体里再创建:
记忆口诀:默认参数永远别用可变对象([] {} set())当默认值,一律用 None 占位再在体内创建。 这条规则没有例外,背下来。
三、循环里的闭包:变量是"晚绑定"的
这个坑 JS 老手会秒懂,因为 var 时代天天踩。
为什么(WHY):闭包捕获的是变量本身的引用,不是定义那一刻的值。等循环跑完,i 已经停在 2,三个 lambda 此时才被调用,读到的都是最终的 i=2。这和 JS var 的行为一模一样:
// JS:var 版本完全同款翻车
var funcs = []
for (var i = 0; i < 3; i++) {
funcs.push(() => i)
}
console.log(funcs.map(f => f())) // [3, 3, 3] —— 同样的坑
JS 的解法是改用 let(块级作用域,每轮新建绑定)。但 Python 没有 let,也没有块级作用域——这就是边界差异:你不能照搬 JS 的修法。Python 的解法是用默认参数在定义时"快照"当前值:
有意思的是:第一节"默认参数只求值一次"是个坑,这里反过来利用同一个机制来解题。理解了机制,坑和技巧是一体两面。
边界提醒:Python 的循环变量
i在循环结束后依然存在(没有块级作用域),这点也和 JS 的let不同——别以为出了for它就消失了。
四、is vs ==:身份相等 vs 值相等
JS 里你纠结 == 和 ===;Python 里换成纠结 == 和 is,但对应关系不是你以为的那样。
先建立锚点,再立刻划清边界:
| 比较 | JS | Python | 比的是什么 |
|---|---|---|---|
| 值相等 | ==(带隐式转换) |
==(不隐式转换) |
内容是否相等 |
| 严格相等 | === |
—— | (Python 的 == 已经不转换类型) |
| 引用/身份相等 | ===(对象比较引用) |
is |
是不是同一个对象 |
关键差异:Python 的 == 不做 JS 那种离谱的隐式类型转换(不会有 [] == ![] 这种鬼故事),所以日常比较值用 == 就对了。is 比的是"是不是内存里同一个对象",约等于 JS 对象用 === 比引用。
最大的坑:用 is 比较数字/字符串,会被"小整数缓存"骗到。 Python 把 -5 到 256 的小整数预先缓存复用,导致 is 时灵时不灵。下面用**交互式 REPL(逐行输入)**演示,这样才能稳定看到差异:
注意:这个 False 只在逐行输入的 REPL 里稳定出现。 如果把 x = 257 / y = 257 写进同一个 .py 脚本一起运行,编译期的"常量折叠"可能让 257 is 257 也变成 True——也就是说结果会随运行方式而变(CPython 实现细节,别依赖)。
正因为 is 的结果如此不可靠,所以比较值永远用 ==,绝不用 is。is 只用在一个地方:和单例 None / True / False 比较。
记忆口诀:is None 是唯一该用 is 的高频场景;比较内容一律 ==。
五、深浅拷贝:b = a 不会复制内容
这个坑 JS 程序员完全有共鸣——const b = a 对对象/数组只是复制引用,改一个动另一个。Python 一模一样。
// JS 同款行为
const a = [1, 2, 3]
const b = a // 只复制引用
b.push(4)
console.log(a) // [1, 2, 3, 4]
那怎么真正复制?这里 Python 比 JS 多分了两层概念——浅拷贝 vs 深拷贝:
浅拷贝(shallow):只复制最外层容器,里面的子对象还是共享引用。常见写法有 copy.copy(x)、list(x)、x[:]、dict(x)、{**x}。
深拷贝(deep):递归复制所有层级,得到一棵完全独立的新树,用 copy.deepcopy(x)。
对照 JS 你就秒懂——这正是 JS 里"浅拷贝 vs 真·深拷贝"的同一组概念:
| 操作 | JS | Python | 复制深度 |
|---|---|---|---|
| 引用赋值(不拷贝) | const b = a |
b = a |
0 层,同一个对象 |
| 浅拷贝 | [...a] / {...a} / Object.assign({}, a) |
a[:] / list(a) / {**a} |
1 层 |
| 深拷贝 | structuredClone(a) |
copy.deepcopy(a) |
全部层级 |
边界提醒:JS 的 [...a]、{...a} 也是浅拷贝,嵌套对象同样共享——所以这个"浅拷贝只管一层"的坑你其实在 JS 早见过,规则完全迁移得过来。需要彻底独立时,JS 用 structuredClone,Python 用 copy.deepcopy。
六、附赠几个高频小坑(认脸即可)
这些不展开,先混个眼熟,遇到不至于懵:
| 小坑 | JS 直觉 | Python 真相 |
|---|---|---|
/ 除法 |
7/2 === 3.5 |
一致,但 4/2 也是 2.0(float);要整数用 // |
| 链式比较 | 1 < 2 < 3 在 JS 是 bug |
Python 原生支持,是正确语法 |
and/or |
` | |
| 可变对象做 key | 对象可当 Map 的 key | list/dict/set 不可哈希,要用 tuple |
七、总结
- 切入点:你早就被语言坑过:Python 的坑结构上一一对应,只是触发点换了位置。
- 可变默认参数:函数的"默认值"只生成一次:这是 Python 独有、JS 没有对应物的头号坑,新手几乎必踩。
- 循环里的闭包:变量是"晚绑定"的:这个坑 JS 老手会秒懂,因为 var 时代天天踩。
- is vs ==:身份相等 vs 值相等:Python 里换成纠结 == 和 is,但对应关系不是你以为的那样。
- 深浅拷贝:b = a 不会复制内容:这个坑 JS 程序员完全有共鸣——const b = a 对对象/数组只是复制引用,改一个动另一个。
- 附赠几个高频小坑(认脸即可):| / 除法 | 7/2 === 3.5 | 一致,但 4/2 也是 2.0(float);