Skip to content

Latest commit

 

History

History
199 lines (121 loc) · 6.81 KB

02-troubleshooting.md

File metadata and controls

199 lines (121 loc) · 6.81 KB

常见错误排查 — 可变默认参数 / GIL / 循环导入

概述

Python 新手到中级最常踩的坑集合:症状 → 原因 → 修复。每条可独立跳入阅读。

文档信息(用途、难度与维护记录)

📚 文档元数据

属性 内容
模块 10-python-discovery
象限 字典
难度 ⭐⭐
标签 #排查 #陷阱 #GIL #循环导入
更新日期 2026年9月

1. 可变默认参数

症状: 多次调用共享状态,列表/字典"越攒越多"。

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

2. GIL 与并发选型

症状: 多线程跑 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 年生产环境仍按上表选型即可。


3. 循环导入(circular import)

症状: ImportError: cannot import name 'X' from partially initialized module。

# models.py: from storage import save      # storage 又导入 models → 可能访问尚未初始化的名字
# storage.py: from models import Bookmark

修复(按优先级):

  1. 抽取第三模块:共同依赖下沉到 base.py(根治)
  2. 类型注解延迟:from __future__ import annotations + if TYPE_CHECKING: from models import Bookmark
  3. 函数内导入:把 import 移到调用处(局部规避,勿滥用)

4. UnboundLocalError

症状: 明明有全局变量,函数里却说未绑定。

count = 0
def bump():
    count += 1        # ❌ 赋值使 count 成为局部变量,读取时未初始化

修复: 函数内写 global count(或改传参返回值——更推荐)。


5. 浅拷贝假象

症状: b = a[:] 后原地修改 b[0] 指向的可变对象,a[0] 也能看到变化;给 b[0] 重新赋值则不同——切片/copy() 只复制外层容器。

修复: 嵌套结构用 copy.deepcopy(a)。


6. 编码与 UTF-8

症状: Windows 上读文件出现 UnicodeDecodeError 或乱码。

原因: open 的默认文本编码受 Python 版本、UTF-8 模式和区域设置影响,不能把 Windows 一概视为非 UTF-8。

修复:先确定文件实际编码;项目约定 UTF-8 时显式指定它,遗留编码文件使用匹配的编码再转换。强行按 UTF-8 解码所有输入不能消除编码错误。


7. 浮点精度

症状: 0.1 + 0.2 != 0.3。

修复: 比较用 math.isclose(a, b);金额用 decimal.Decimal("0.1")(字符串初始化)。


8. is vs ==

症状:相同整数用 is 比较有时为 True、有时为 False;结果会受常量复用、执行方式与解释器实现影响。

原因: 小整数缓存(-5~256)是解释器优化副作用,is 比较的是身份而非值。

修复: 值比较一律 ==;is 仅用于 is None 等单例。


9. 迭代中修改容器

症状: 遍历 list 时删除元素导致跳项;遍历 dict 时增删键抛 RuntimeError。

修复: 遍历副本 for x in list(xs):,或用推导式构建新容器。


10. 协程里的阻塞调用

症状: FastAPI 服务在 async def 路由中调用 requests/time.sleep,整个服务卡顿。

原因: 事件循环单线程,无 await 让出则无法调度其他任务。

修复: 换异步等价物(httpx/asyncio.sleep);无法替换时把该路由改成 def(自动跑线程池)。


11. TypeError: 'NoneType' object is not ...

症状: 链式调用中间某步返回了 None(常见于 sort()、append() 这类原地操作)。

xs = xs.sort()        # ❌ sort 原地排序返回 None
xs = sorted(xs)       # ✅ sorted 返回新列表

修复:逐个确认返回契约。sort/append 返回 None,但 pop 原地修改并返回元素;不能把所有原地操作概括为同一返回值。


快速定位方法论

  1. 读栈底再读栈顶:最后一行是异常类型与消息,第一段是你代码的入口
  2. 最小重现:把报错代码缩到 REPL 能复现的最小片段
  3. 类型三查: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],此时改变的是副本中的元素引用,不会修改原内层列表。两种操作的差异比笼统说“浅拷贝不安全”更有用。

排障结论应留下一个能重复执行的用例。涉及时间、网络和并发的问题,记录可验证的性质或事件顺序,避免把某次日志顺序、某台机器的整数缓存或默认编码当成语言保证。

🔗 相关文档

阅读导航

本模块理解地图 · 完整目录与版本 · 通用术语