📑 本页目录(点开跳转)
附录A · 速查
⏱ 52 分钟 | ⭐ 把报错原文和「它到底在说什么」排在一起,Ctrl+F 就能查
📌 这一页是查的,不是读的。所有条目都能指回某一章。
🎯 一句话
正文各章负责让你懂,这一页负责让你快。 它只做三件事:把报错原文按「你看到什么」排好,把症状按「大概率是哪一章」排好,把几条没有例外的规矩放在一处。
⚠️ 页里所有数字都来自各章的实跑输出,机器是 Windows 11 + CPython 3.13。绝对值会随机器变,要看的是倍数和方向。
🧯 一、报错原文 → 它到底在说什么
用法:把你看到的报错开头几个词在这一节 Ctrl+F。
TypeError: object of type 'generator' has no len()
生成器没有长度,因为它根本不知道自己有多少个 —— 值是边走边产的。
→ 真要计数就 sum(1 for _ in g),⚠️ 但那一次遍历会把它耗光。详见 02 · 生成器与惰性求值第三节。
TypeError: retry.<locals>.decorator() missing 1 required positional argument: 'fn'
⭐ 判据是错误信息里出现了装饰器内部的函数名(decorator / wrapper):八成是带参数的装饰器少写了那对括号,@retry 应该写成 @retry(3)。
⚠️ 装饰那一步不报错,报错发生在第一次调用被装饰的函数时。详见 03 · 装饰器与上下文管理器第三节。
RuntimeError: generator didn't stop
@contextlib.contextmanager 修饰的生成器里有两个 yield。一个上下文管理器只管一对进出。
→ 数一遍 yield 的个数。要嵌套就嵌套 with,或者用 ExitStack。详见 03 章第十节。
TypeError: 'classmethod' object is not callable / C.helper() takes 1 positional argument but 2 were given
⭐ @staticmethod / @classmethod 没写在最外层。 装饰器叠起来是从下往上应用的,你自己的 @log 写在外面时,类字典里存的就是普通 function 而不是 staticmethod。
→ 验证一行:type(C.__dict__["helper"]) 应该是 staticmethod。⚠️ 从类上调可能是对的,从实例上调才炸 —— 所以它能躲过一半的测试。详见 03 章第五节。
AttributeError: 'NoneType' object has no attribute ...(紧跟在 with ... as x 之后)
__enter__ 里忘了 return self,as x 拿到的是 None。⭐ as x 绑的是 __enter__ 的返回值,不是那个对象本身。
→ 在 with 块第一行 print(x) 就能确认。详见 03 章第六节。
RuntimeError: An attempt has been made to start a new process before the current process has finished its bootstrapping phase
训练脚本 / 建进程池那一行没被 if __name__ == "__main__" 守住,子进程重新 import 主模块时又执行了一遍,无限递归。
💀 难查的原因:你在终端里先看到的是 BrokenProcessPool: A process in the process pool was terminated abruptly —— 字面像资源不足,而真正的 RuntimeError 埋在一堆 multiprocessing/spawn.py 栈帧中间;在 Notebook / IDE 里子进程的 stderr 常常根本不显示。详见 04 · GIL第五节。
PicklingError / TypeError: cannot pickle ...
要送进子进程的东西不能被 pickle:lambda、局部函数、打开的文件句柄、数据库连接、threading.Lock 都不行。详见 04 章第六节。
ValueError: invalid literal for int() with base 10: 'n/a'
⭐ 报错已经告诉了你是哪个值坏了('n/a')。别急着往上翻,先看最后一行。
⭐ 3.11 之后 traceback 里那几行 ~~~^^^ 会指出一行里到底是哪个调用炸的 —— return int(v) / 100 底下标在 int(v) 上,说明不是除法的问题。详见 07 · 异常、traceback 和调试第一节。
ModuleNotFoundError: No module named 'xxx'
⚠️ 它只告诉你「没找到」,从不告诉你「在哪找过」。 自己打出来:
python -c "import importlib.util as u; print(u.find_spec('xxx'))",再打 sys.path。
⭐ 十次里有一次是名字问题:pip install pyyaml → import yaml;scikit-learn → sklearn;opencv-python → cv2;Pillow → PIL。去 PyPI 页面抄那行 import,别猜。 详见 08 · 环境、依赖和 import第一节。
UnicodeDecodeError: 'gbk' codec can't decode byte 0x8c in position 14
⭐ 你从头到尾没提过 GBK,是 open() 替你选的。 Windows 上 open() 用的是 locale.getpreferredencoding(False)(本机 cp936),不是 sys.getdefaultencoding()(那个管的是 str/bytes 互转,跟读文件没关系)。
→ 一律显式写 encoding="utf-8"。详见 10 · 编码、路径和文件第一节。
UnicodeEncodeError: 'gbk' codec can't encode character '\u2705'
你 print 了一个 emoji,而控制台编码不是 UTF-8。
💀 本项目的真实事故:构建最后一步因为打印一个对勾崩了,而外层的 except 打印的是「不影响其他内容」—— 在站点已经坏掉时报了平安,全站 156 张图当场只剩 75 张。
→ 脚本开头钉死输出编码,并且 errors="replace" 那半句同样重要:打个问号也别抛异常。详见 10 章第三节。
SyntaxWarning: invalid escape sequence(Windows 路径里)
"C:\data\new\test.txt" 里的 \n \t 已经变成换行和制表符了。⭐ 3.12 起解释器会先警告你一句 —— 别忽略它,那就是在报这个坑。
→ 用 r"C:\data\new",或者干脆用正斜杠(Windows API 两种都收),最好用 pathlib。详见 10 章第六节。
AssertionError 在生产上「消失了」
⚠️ 加了 -O(或 PYTHONOPTIMIZE=1)之后,assert 那一行在编译期就被整条删掉,不是「不抛异常」。实跑对照:不带参数退出码 1、报 AssertionError;带 -O 退出码 0,负数一路进了数据。
→ 校验外部数据一律用 if not ...: raise ValueError(...),assert 只用来写「我认为这里不可能发生」。详见 07 章第四节。
没有报错,但 undefined symbol / DLL load failed / GLIBC_2.32 not found
⭐ 这一整类不归本板块。 分界线很清楚:报错里出现文件路径是 08 章;报错里出现符号名 / 版本号 / .so / .dll 是
框架底下是 C++ · 04 · ABI 与二进制兼容。
🔍 二、症状 → 大概率是哪一章
⭐ 没有报错的那些,全在这张表里。 这是本页最该扫一遍的一节。
| 你观察到的现象 | 大概率是 | 一行验证 |
|---|---|---|
改了 b,a 跟着变 |
01 赋值只是贴标签 | print(a is b) |
| 函数调用完,传进去的东西被改了 | 01 形参是同一个对象的又一个名字 | 改成返回新对象 |
| ⭐ 函数第二次调用结果就不对 | 01 可变默认参数 | print(f.__defaults__) |
| 改了内层,拷贝出来那份也变了 | 01 浅拷贝只复制外层 | print(new["k"] is old["k"]) |
| 模型 / 缓存 / 计数器在多个实例间串了 | 01 类属性被所有实例共享 | print(a.attr is b.attr) |
| ⭐ 判断时对时错,换个输入就不灵 | 01 拿 is 当 == 用 |
把 is 换成 == |
| ⭐ 第二个统计量总是 0 / 空列表,不报错 | 02 生成器只能走一次 | 第一次遍历前 rows = list(rows) |
| 参数明明不合法,调用却没抛异常 | 02 校验被推迟到第一次 next |
拆两层:普通函数校验 + 内层生成器产数据 |
| 流式接口出错,返回的是 200 不是 500 | 02 状态码早发出去了 | 把校验挪到返回响应对象之前 |
| 分组结果「键都对、内容全空」 | 02 groupby 的惰性游标 |
别 list(groupby(...)),在循环体里当场消费 |
| 用了生成器但内存没降 | 02 最里层已经物化 | 找 readlines() / list() / sorted() |
日志里函数名全是 wrapper |
03 忘了 functools.wraps |
print(f.__name__) |
⭐ @tool 生成的 schema 全错 / FastAPI 校验形同虚设 |
03 签名被吃成 (*args, **kwargs) |
print(inspect.signature(f)) |
| 💀 数据静默少一半,没有任何 traceback | 03 __exit__ 返回了真值 |
删掉 return True,改用 contextlib.suppress |
| 出异常时连接没关 / 锁没放 | 03 @contextmanager 里没写 try/finally |
看 yield 是不是裸的 |
| 函数明明写了,框架说找不到 | 03 模块没被 import,装饰器没跑 | 检查注册模块有没有真的被 import |
| 开了 8 个线程,CPU 还是只占一个核 | 04 活在解释器里跑 | 换 4 进程再测,快了就是 GIL |
| 同事说线程池管用,我这儿不管用 | ⭐ 04 他的活放 GIL,你的不放 | 串行 / 4 线程各测一次,看倍数 |
| 4 个进程比串行慢十几倍 | 04 pickle 的账 | len(pickle.dumps(arg)) |
| 顶层的模型加载被执行了 5 次 | 04 spawn 会重新 import 主模块 | 顶层打一行 print(os.getpid()) |
| 计数器偶尔少几条,重跑就好了 | ⭐ 04 GIL 不等于线程安全 | sys.setswitchinterval(1e-6) 放大概率 |
| 「我改完快了 30%」但换台机器就没了 | 05 单次测量没有信息 | 同一句连测 5 次,本底抖动就有 2.1 倍 |
| 第二次测「快了 8 万倍」 | 05 lru_cache 让你测的不是同一件事 |
每轮换输入,或 fn.cache_clear() |
| ⭐ 「种子固定了,结果还是不一样」 | 09 set 的迭代顺序每次进程启动都变 |
见下面第五节 |
Counter 出来只有两类而不是四类 |
09 1 / 1.0 / True 是同一个 key |
print({1:'a', True:'b'}) |
本地跑得好好的,上 Linux 就 FileNotFoundError |
10 Windows 文件名大小写不敏感 | 文件名一律小写 |
| 同一个脚本换个目录跑,读到的文件不一样 | ⭐ 10 相对路径相对的是工作目录 | print(os.getcwd()) 对比 __file__ |
📋 三、几条没有例外的规矩
| 场合 | 规矩 | 不守会怎样 |
|---|---|---|
| 比较 | 只有和 None / True / False 比才用 is,其余一律 == |
⭐ 小整数缓存(−5~256)和字面量合并让 is 时对时错 |
| 默认参数 | 默认值只能是不可变对象;要空列表就写 None 再在函数体里造 |
默认值在定义那一刻求值一次,被所有调用共用 |
| 装饰器 | 写包装函数一律加 functools.wraps |
函数名、docstring、签名全被吃掉,schema 生成器和框架校验跟着废 |
| 上下文管理器 | @contextmanager 里 yield 必须包在 try/finally 里 |
出异常时清理代码一次都不会跑 |
| 异常转抛 | 默认写 raise X from e |
from None 会把根因整段删掉,只剩一句没用的业务话 |
except 范围 |
⭐ 白名单,不是黑名单 | except Exception: retry 会把「prompt 超长」这种确定性错误也重试三遍 |
| 校验 | 外部数据用 raise,assert 只写内部不变量 |
-O 下 assert 整条消失 |
| 打开文本文件 | 一律显式 encoding="utf-8"(read_text / write_text 也要) |
不写不是「最好写」,是「bug 还没被触发」 |
| 路径 | pathlib;要读「和脚本放一起」的文件用 Path(__file__).resolve().parent |
💀 本项目栽过:硬编码 ROOT,读新的写旧的,两边都不报错 |
| 进程池 | 建池那一行必须在 if __name__ == "__main__" 里面 |
子进程重新 import 主模块 → 无限递归 |
⚠️ 裸 except: 和 except Exception 差在哪:前者连 KeyboardInterrupt(Ctrl-C)和 SystemExit 一起吞掉 —— 那两个不是 Exception 的子类。想让程序还能被停下来,就别写裸的。
⏱️ 四、量一段代码:工具和参数
| 想知道 | 用哪个 | ⚠️ |
|---|---|---|
| 一段代码跑了多久 | ⭐ time.perf_counter(),无一例外 |
单调,不会被 NTP / 夏令时 / 虚拟机挂起改掉 |
| 这段时间 CPU 真为我干了多少活 | time.process_time() |
⭐ 两者的差值就是「花在等待上的时间」,它本身就是诊断信息 |
| 现在几点、日志时间戳 | time.time() |
只记录时刻,不用来算间隔 |
| 比较两种写法 | timeit.repeat(...) 取最小值 |
噪声只往上加不往下减;命令行版直接印 best of 5 |
| 时间花在哪个函数 | cProfile + pstats |
见下面三条 |
| 内存峰值 | tracemalloc(要的是峰值不是结束时) |
杀死进程的是峰值 |
⭐ timeit 三个参数:number 是「一轮跑几次」(让一轮总时长落在 0.1~1 秒),repeat 是「跑几轮」(至少 5),setup 里的东西不计时。
⚠️ repeat(..., repeat=15, number=20) 返回 15 个数,每个是 20 次的总和 —— 忘了除以 number,你所有数字会整整大 20 倍而且看起来毫无破绽。
📋 剖析报告怎么读:
| 列 | 含义 | 什么时候排它 |
|---|---|---|
tottime |
这个函数自己用掉的,不含子调用 | 找真正的热点,决定改哪一行 |
cumtime |
从进到出总共过去多久,含子调用 | 找该整块砍掉的环节 |
⭐ ncalls |
被调了几次 | 常常比 tottime 更有用 —— 该砍的是那 20 万次调用,不是那个函数 |
💀 剖析器自己要钱,而且收费不均:实跑里一百万次函数调用的那段被放大 5.9 倍,同样一百万次的纯循环 1.0 倍(几乎没变)。裸跑 0.049 : 0.031 到了报告里变成 0.290 : 0.029。⭐ 排序没变,比例被夸张了 6 倍 —— 所以用剖析器定位、用 timeit 验收,是两件事两套工具。
⚠️ 三样会让你测出假数字的东西(05 章实跑):① 一次性的钱混进来了(import numpy 一次就要 0.174 秒,而被测的 a.sum() 只有 2 毫秒);② 缓存让你第二次量的不是同一件事(lru_cache 下「快了 8 万倍」);③ timeit 默认把 GC 关了(同一段代码 2.21 ms → 2.69 ms,差 22%),而你的程序里 GC 是开着的。
🚦 五、并发怎么选,以及可复现的两件事
⭐ 判据只有一句:这段活里有没有在造 Python 对象。 有,就抢 GIL,线程没用;没有,C 扩展会把锁放开,线程真能并行。
| 你的活 | 选什么 |
|---|---|
| 在等网络 / 磁盘,且库是 async 的 | ⭐ async |
在等,但库是同步的(requests、老驱动) |
⭐ 线程池(别为了 async 硬换库) |
| 在算,算的是 numpy / torch / 压缩 / 哈希 | ⭐ 线程池 —— 底层放锁,还不用付 pickle 的钱 |
| 在算,纯 Python(解析、循环、正则) | ⭐ 进程池 |
| 在算,纯 Python,但数据非常大 | ⚠️ 先算 pickle 的账 |
实测倍数(同一函数,串行一次、4 线程一次,取最小值):
| 操作 | 串行 | 4 线程 | 倍数 | 放不放锁 |
|---|---|---|---|---|
np.sort 排 800 万 float |
0.80 s | 0.25 s | 3.2× | ✅ 放 |
zlib.compress 压 10 MB |
0.240 s | 0.086 s | 2.78× | ✅ 放 |
hashlib.sha256 摘要 50 MB |
0.174 s | 0.110 s | 1.58× | ✅ 放(数据小时收益被调用开销吃掉) |
json.loads 解析 13.7 MB |
1.51 s | 1.45 s | 1.04× | ❌ 不放(全程在造 dict / list / str) |
re.findall 在 320 万字符上 |
0.223 s | 0.217 s | 1.03× | ❌ 不放 |
⭐ 不确定就量:同一个函数串行一次、四线程一次,倍数接近核数就是放锁的,接近 1 就是不放的。
💀 「种子固定了、结果还是不一样」的那个洞:set 的迭代顺序取决于哈希种子,而哈希种子是解释器启动时读一次就定死的。所以在 set_seed() 里写 os.environ["PYTHONHASHSEED"] = "0" 一点用都没有 —— 实跑里 hash('a') 照样三次三个值,而且会让你以为已经修好了。
| 正确设法 | 写法 |
|---|---|
| ⭐ 命令行 | PYTHONHASHSEED=0 python train.py(PowerShell 先 $env:PYTHONHASHSEED=0) |
| 容器 / CI | ENV PYTHONHASHSEED=0 |
| 脚本自己重启 | 检测到没设就带着 env os.execve 重起自己 |
🗃️ 六、容器:复杂度、顺序、能不能当 key
| 操作 | list |
tuple |
dict |
set |
deque |
|---|---|---|---|---|---|
x in c |
O(n) | O(n) | O(1)* | O(1)* | O(n) |
| 按下标 | O(1) | O(1) | — | — | O(n)(两端 O(1)) |
| 尾部追加 | O(1) 均摊 | 不可变 | — | O(1)* | O(1) |
| 头部插入 / 弹出 | O(n) | 不可变 | — | — | ⭐ O(1) |
| 按值 / 按键删除 | O(n) | 不可变 | O(1)* | O(1)* | O(n) |
| 能当 dict 的 key | ❌ | ✅(内部全不可变时) | ❌ | ❌(用 frozenset) |
❌ |
| 迭代顺序 | 位置序 | 位置序 | ⭐ 插入序(3.7+) | ⚠️ 无保证 | 位置序 |
* 平均情况,大量碰撞时退化成 O(n)。
⭐ 最高频的一次改写:把被查的那一侧先转成 set。实跑 2 万 × 2 万规模,x in list 1.6106 秒 → x in set 0.0021 秒,快 774 倍,结果一模一样。
⚠️ 但 set(other) 本身是 O(n):只查一次时白转,而且必须把它提到循环外面 —— 写成 if x in set(other) 等于每轮重建一次集合,比原来还慢。
⭐ 能当 key 的两条契约:① 能算 hash();② ⚠️ 在它当 key 期间 hash 值不许变(字典先按哈希找槽再用 == 确认,哈希变了等于换了个槽去找,原来那格永远够不着)。写 __hash__ 时和 __eq__ 用同一批字段。
💀 {1: 'int', 1.0: 'float', True: 'bool'} 实跑得到 {1: 'bool'} —— 三个 key 塌成一个,而且不对称:key 保留最先插入的(int 的 1),value 用最后写入的。标签列里混了 True/False 和 1/0 时,Counter 出来只有两类而不是四类,没有任何报错。
📦 七、「我这能跑,你那不能跑」:五步定位
⭐ 照这个顺序问,五步之内能定位到具体是哪一类(08 章第八节):
| 步 | 命令 | 排除掉什么 |
|---|---|---|
| ① | python -c "import sys; print(sys.executable)" |
⭐ 两边是不是同一个解释器。八成的问题在这一步就结束 |
| ② | python -c "import sys; print(sys.prefix != sys.base_prefix)" |
有没有真的在 venv 里(激活了 ≠ 你的工具在用它) |
| ③ | python -c "import 那个包 as m; print(m.__file__)" |
是没装,还是导到了另一个同名的东西 |
| ④ | python -m pip list |
版本对不对。⚠️ 一定带 -m |
| ⑤ | python -c "import sys; print(*sys.path, sep=chr(10))" |
路径顺序、有没有莫名其妙的 PYTHONPATH |
⚠️ sys.path[0] 排在标准库前面 —— 你自己写的 random.py / json.py 会顶掉标准库。而 [0] 是入口决定的:python x.py 是 x.py 所在目录,python -m pkg 是当前工作目录。
💡 两个附加动作:怀疑缓存就删 __pycache__(它认的是 mtime 和字节数,不是内容);import 慢就 python -X importtime -c "import 那个包"。
🧠 八、让解释器多说一点
| 开关 | 干什么 | 什么时候用 |
|---|---|---|
⭐ -X dev |
把平时静音的 ResourceWarning 等放出来 |
「文件句柄没关」会一路积累到 Too many open files,而那个报错的位置和真正的泄漏点完全无关 |
-W error |
把警告变成异常 | 想拿到 DeprecationWarning 出事的那一行,而不是一行字 |
-X importtime |
按毫秒列出每个子模块的导入耗时 | 启动慢 |
-O |
⚠️ 删掉所有 assert |
知道它存在就行,别依赖 assert 做校验 |
breakpoint() |
调 sys.breakpointhook() |
⭐ 可以换掉那个 hook,做成「不打断执行的观察点」 |
📋 真进了 pdb,六个命令够用:l(看源码,先搞清自己在哪)· p / pp(打印)· n(下一行,不进函数)· s(进函数)· c(继续)· w(打印调用栈)。
⭐ 最常用的组合是 w → u → p 变量:先看栈,用 u 跳到你自己的那一层,再看变量。
🔖 九、类型标注:词汇表和该标哪
| 写法 | 意思 |
|---|---|
list[str] dict[str, int] |
⭐ 3.9+ 直接用内置名,不用 typing.List |
str 竖线 None |
要么 str 要么 None,等价于老写法 Optional[str] |
Literal["fast", "quality"] |
⭐ 只能是这几个字面值 —— 在 Agent 工具里直接变成 JSON schema 的 enum,模型就不会瞎编第三个值 |
Callable[[str], int] |
收一个 str 返回 int 的函数 |
Any |
⚠️ 它关掉这一处的检查,不是「什么都行」的安全写法 |
⭐ 该不该标,判据只有一条:这个函数的类型信息会不会被「你以外的人或程序」读? 一定标:包的对外函数、Agent 工具函数(标注直接变成 schema)、Web 请求/响应模型、数据类。 别标:一次性实验脚本、notebook 单元格、局部变量。
⚠️ 标注 ≠ 校验。mypy 在代码没运行时读源码,挡的是「我把 None 传给了要 str 的函数」;pydantic 在每次构造对象时跑,挡的是「用户传了 max_tokens: "很多"」。⭐ 最坏的形态是过时的标注 —— 它比没有标注更糟,因为读的人会信它。标了就要让 CI 跑检查。
🔗 这一页连到哪里
| 相关的地方 | 为什么 |
|---|---|
| 00 · 怎么用这份教程 | 三条读法路线和四条分工线(什么归这里、什么不归)在那里定义 |
| 04 · GIL:为什么多线程救不了你 | 第五节那张「放不放锁」表的完整版,含每一行怎么自己量 |
| 08 · 环境、依赖和 import | 第七节那五步的展开,以及 venv 到底只做了哪一件事 |
| NumPy 与向量化思维 · 附录A 速查 | ⭐ 同样是报错反查页,那边覆盖广播、视图/拷贝、dtype 溢出 —— 数组相关的报错去那边查 |
| 框架底下是 C++ · 04 · ABI 与二进制兼容 | ⭐ 「装不上」的另一半:报错里出现符号名、.so / .dll、glibc 版本号时,本页第一节的最后一条把你送去那里 |
| 代码题拆解 · 附录A 速查 | 同一批容器(deque / Counter / heapq)在刷题场景里的固定用法 |
| AI 全栈 · 15b · 怎么测不确定的系统 | 本页第三节说「测试里的断言用 assert 合适」,那一章讲这类测试整体怎么组织 |
🗓️ 这一页会过期的部分:Python 3.15 起 open() 默认编码会改成 UTF-8(PEP 686);GIL 的去留还在变。
⭐ 不会过期的部分:第二节那张症状表、第三节那十条规矩 —— 它们由对象模型和执行顺序决定,比任何一个小版本活得久。
下一节 👉 回到板块索引