🏠 总目录📚 本教程 附录A · 报错反查与速查 ←
📑 本页目录(点开跳转)

附录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 的去留还在变。 ⭐ 不会过期的部分:第二节那张症状表、第三节那十条规矩 —— 它们由对象模型和执行顺序决定,比任何一个小版本活得久。

下一节 👉 回到板块索引

打卡记录保存在你的浏览器里,首页能看到总进度