前沿与实践 · Frontier & Practice
lv.4 前沿
kp-047
常见误区与陷阱集
1. 一句话定义
本篇把散落在各知识点的"坑"汇成一张复习用速查表——每条都指向机制层解释,建议在阶段二/三结束时各过一遍。
2. 为什么重要
90% 的 Python bug 反复来自同一批机制误解。把它们集中复习一次的收益,远大于再学一个新语法特性;这也是面试的经典题库。
3. 前置知识
4. 核心概念(陷阱速查表)
| # | 陷阱 | 最小示例 | 机制根源 | 详见 |
|---|---|---|---|---|
| 1 | 可变默认值 | def f(x=[]) | 默认值定义时求值一次 | kp-007 |
| 2 | b = a 以为复制 | b.append 波及 a | 名字绑定 | kp-003 |
| 3 | 浅拷贝嵌套坑 | a[:] 共享内层 | 浅复制语义 | kp-028 |
| 4 | 迭代器二次消费 | sum(it) 两次 | 协议一次性 | kp-012 |
| 5 | 迟绑定闭包 | 循环里 lambda 共享 i | 捕获变量非值 | kp-023 |
| 6 | is 比值 | 256 is 256 偶真 | 小整数缓存 | kp-005 |
| 7 | sort 返回 None | lst = lst.sort() | 原地方法约定 | kp-009 |
| 8 | 类属性可变共享 | members = [] 类级 | 属性查找顺序 | kp-017 |
| 9 | 循环中删列表元素 | 边遍历边 remove | 迭代器视图错位 | kp-006 |
| 10 | except: 吞一切 | 裸 except | BaseException 层 | kp-015 |
| 11 | GIL 下线程加速 CPU | 线程池跑计算 | 字节码级单锁 | kp-029 |
| 12 | 协程忘 await | f() 未执行 | 协程对象惰性 | kp-031 |
| 13 | f-string 拼日志 | log(f"{heavy()}") | 先求值后传参 | kp-037 |
| 14 | groupby 不排序 | 相邻才分组 | 惰性分组语义 | kp-038 |
| 15 | dict 遍历中增删 | RuntimeError | 哈希表结构变更 | kp-010 |
| 16 | == 比 float | 0.1+0.2==0.3 假 | IEEE754 | kp-004 |
| 17 | 默认编码漂移 | Windows GBK | 平台默认编码 | kp-016 |
| 18 | 忘 __main__ 守卫 | 多进程递归爆 | spawn 重新 import | kp-032 |
5. 原理与机制(三个"母题")
绝大多数陷阱收敛于三个母题,记住母题比记 18 条更有效:
text
母题 A【共享与绑定】:一切"改了不该改的"都源于
多个名字引用同一个可变对象(kp-003)
母题 B【求值时机】:一切"为什么当时不是这个值"都源于
定义时求值 vs 使用时求值的错位(默认值/闭包/装饰器)
母题 C【协议与惰性】:一切"怎么没反应/怎么只跑一次"都源于
迭代器一次性、协程惰性、描述符拦截时机6. 关键事实(模型/图示)
text
自我检查清单(写完代码过一遍):
□ 有没有可变默认值/类属性可变共享?
□ 有没有浅拷贝承担了深拷贝的期待?
□ 迭代器/生成器有没有被二次消费?
□ 循环 lambda 是否捕获了循环变量?
□ 异常处理是否过宽?try 范围是否过大?
□ 并发代码的共享状态有没有锁/队列?
□ 异步函数里的阻塞调用清零了吗?7. 直观类比
陷阱集像考驾照的违章扣分表:不是为了背下来应付考试,而是每一条背后都是一次真实事故(机制解释);过一遍扣分表(本篇)比重新撞一遍墙便宜得多。
8. 实例与案例(三个最经典的最小复现)
python
# ① 可变默认值
def add(x, bag=[]): bag.append(x); return bag
add(1); add(2) # [1, 2]——跨调用累积
# ② 迟绑定
fs = [lambda: i for i in range(3)]
[f() for f in fs] # [2, 2, 2]
# ③ 迭代器耗尽
it = iter([1, 2])
sum(it), sum(it) # (3, 0)9. 常见误区(关于误区本身的元误区)
- 背答案不背机制 —— 变体一出就露馅;每条陷阱都要能推回 kp-003/007/012。
- 把"运行没报错"当"没踩坑" —— 陷阱多是语义错误而非语法错误,静默出错才致命。
- 只在此清单内复习 —— 真正的疫苗是 wtfpython 与 Fluent Python 的完整案例库。
10. 自测题
- 用"母题 B"解释默认值陷阱与闭包迟绑定的共同根源。
- 为什么
sort()返回 None 是有意的 API 设计而非疏忽? - 陷阱 11 与 12 的共同点是什么?
参考答案
- 都是定义时求值:默认值在 def 时求值一次被共享;lambda 捕获的变量在循环结束时才被读取——"值取的时机"与"预期时机"错位。
- 强制区分"原地修改"与"返回新对象"两族 API:返回 None 让你无法把原地修改误当表达式使用(Pythonic 明确性优于便利性)。
- 都属于母题 C(惰性):线程按调度交错执行(等待也是惰性),协程对象创建后不自动执行。
11. 与其他知识点的关系
12. 延伸阅读
- wtfpython(中英双语):https://github.com/satwikkansal/wtfpython
- Fluent Python 各章 "Gotchas"