Python 新手到中级最常踩的坑集合:症状 → 原因 → 修复。每条可独立跳入阅读。
文档信息(用途、难度与维护记录)
| 属性 | 内容 |
|---|---|
| 模块 | 10-python-discovery |
| 象限 | 字典 |
| 难度 | ⭐⭐ |
| 标签 | #排查 #陷阱 #GIL #循环导入 |
| 更新日期 | 2026年9月 |
症状: 多次调用共享状态,列表/字典"越攒越多"。
def add_item(item, items=[]): # ❌ 默认值在定义时创建一次
items.append(item)
return items
add_item("a") # ['a'];再 add_item("b") → ['a', 'b'],期望 ['b']原因: 默认值存于函数对象的 __defaults__,全程只求值一次。
修复:
def add_item(item, items: list | None = None):
if items is None:
items = [] # 每次调用新建
items.append(item)
return items症状: 多线程跑 CPU 密集任务,速度不升反降。
原因: CPython 的全局解释器锁(GIL)保证同一时刻仅一个线程执行 Python 字节码。I/O 等待会释放 GIL(多线程对网络/磁盘有效),纯 Python CPU 计算通常受 GIL 约束,部分原生扩展会释放它;free-threaded 构建的约束另行考虑。
决策表:
| 任务类型 | 方案 |
|---|---|
| 网络/磁盘 I/O 并发 | asyncio(首选)或线程池 |
| CPU 密集 | multiprocessing / ProcessPoolExecutor |
| 兼容阻塞库的过渡 | 线程池 run_in_executor |
from concurrent.futures import ProcessPoolExecutor
with ProcessPoolExecutor() as pool:
results = list(pool.map(cpu_heavy, inputs))附注: free-threaded(no-GIL)构建自 3.13 实验引入,3.14 起转为官方支持的构建选项(PEP 779),但默认解释器仍带 GIL,第三方生态适配持续推进中;2026 年生产环境仍按上表选型即可。
症状: ImportError: cannot import name 'X' from partially initialized module。
# models.py: from storage import save # storage 又导入 models → 可能访问尚未初始化的名字
# storage.py: from models import Bookmark修复(按优先级):
- 抽取第三模块:共同依赖下沉到
base.py(根治) - 类型注解延迟:
from __future__ import annotations+if TYPE_CHECKING: from models import Bookmark - 函数内导入:把 import 移到调用处(局部规避,勿滥用)
症状: 明明有全局变量,函数里却说未绑定。
count = 0
def bump():
count += 1 # ❌ 赋值使 count 成为局部变量,读取时未初始化修复: 函数内写 global count(或改传参返回值——更推荐)。
症状: b = a[:] 后原地修改 b[0] 指向的可变对象,a[0] 也能看到变化;给 b[0] 重新赋值则不同——切片/copy() 只复制外层容器。
修复: 嵌套结构用 copy.deepcopy(a)。
症状: Windows 上读文件出现 UnicodeDecodeError 或乱码。
原因: open 的默认文本编码受 Python 版本、UTF-8 模式和区域设置影响,不能把 Windows 一概视为非 UTF-8。
修复:先确定文件实际编码;项目约定 UTF-8 时显式指定它,遗留编码文件使用匹配的编码再转换。强行按 UTF-8 解码所有输入不能消除编码错误。
症状: 0.1 + 0.2 != 0.3。
修复: 比较用 math.isclose(a, b);金额用 decimal.Decimal("0.1")(字符串初始化)。
症状:相同整数用 is 比较有时为 True、有时为 False;结果会受常量复用、执行方式与解释器实现影响。
原因: 小整数缓存(-5~256)是解释器优化副作用,is 比较的是身份而非值。
修复: 值比较一律 ==;is 仅用于 is None 等单例。
症状: 遍历 list 时删除元素导致跳项;遍历 dict 时增删键抛 RuntimeError。
修复: 遍历副本 for x in list(xs):,或用推导式构建新容器。
症状: FastAPI 服务在 async def 路由中调用 requests/time.sleep,整个服务卡顿。
原因: 事件循环单线程,无 await 让出则无法调度其他任务。
修复: 换异步等价物(httpx/asyncio.sleep);无法替换时把该路由改成 def(自动跑线程池)。
症状: 链式调用中间某步返回了 None(常见于 sort()、append() 这类原地操作)。
xs = xs.sort() # ❌ sort 原地排序返回 None
xs = sorted(xs) # ✅ sorted 返回新列表修复:逐个确认返回契约。sort/append 返回 None,但 pop 原地修改并返回元素;不能把所有原地操作概括为同一返回值。
- 读栈底再读栈顶:最后一行是异常类型与消息,第一段是你代码的入口
- 最小重现:把报错代码缩到 REPL 能复现的最小片段
- 类型三查:
type(x)、repr(x)、id(x)—— 类型、内容、身份;怀疑状态污染时逐段注释二分
前置是能读取异常类型和定位文件行号。先记录解释器路径与版本、完整错误链、最小输入,再缩小复现范围。只记录“报错了”或最后一句文本,容易漏掉真正触发错误的上一层调用。
练习用 a = [[1]]; b = a.copy(); b[0].append(2) 复现共享引用,检查 a is b 为 False、a[0] is b[0] 为 True。再改为 b[0] = [9],此时改变的是副本中的元素引用,不会修改原内层列表。两种操作的差异比笼统说“浅拷贝不安全”更有用。
排障结论应留下一个能重复执行的用例。涉及时间、网络和并发的问题,记录可验证的性质或事件顺序,避免把某次日志顺序、某台机器的整数缓存或默认编码当成语言保证。