import x 找不到、或者找到的不是你想要的那个 x,几乎总是 sys.path 的问题。这篇不背规则,而是用不同的启动方式、一个故意同名的文件,把 sys.path 的来源和搜索顺序真实跑出来。
python proj/show.py (在工作目录下) → '<工作目录>/proj' cd proj && python show.py → '<工作目录>/proj' python -c 'import sys;print(repr(sys.path[0]))' → '' python -m pkg.m (在 proj 目录) → '<工作目录>/proj' python elsewhere/link.py (符号链接指向 proj/show.py) → '<工作目录>/proj' python -P proj/show.py (不自动加入脚本目录) → '<PREFIX>/lib/python314.zip' PYTHONSAFEPATH=1 python -c ... → '<PREFIX>/lib/python314.zip'
运行 python proj/show.py,sys.path[0] 是脚本所在的目录(不是当前工作目录);-c 时是空字符串(表示当前目录);-m pkg.m 是当前目录;通过符号链接启动时,取的是符号链接指向的真实文件所在目录。加 -P(或环境变量 PYTHONSAFEPATH=1)后,不再自动加入这一项,sys.path[0] 就变成了标准库的 zip 条目。
(PYTHONPATH 指向 <工作目录>/pp)
0 <工作目录>/proj
1 <工作目录>/pp
2 <PREFIX>/lib/python314.zip
3 <PREFIX>/lib/python3.14
4 <PREFIX>/lib/python3.14/lib-dynload
5 ~/Library/Python/3.14/lib/python/site-packages
6 /opt/homebrew/lib/python3.14/site-packages顺序是:脚本目录 → PYTHONPATH 的各项 → 标准库(python314.zip、lib/python3.14、lib-dynload)→ 用户 site-packages → 系统 site-packages。越靠前优先级越高,同名模块先命中的赢。第五节的 sysconfig 输出可以对上标准库和 site-packages 的具体位置。
shadow/ 里有一个自己写的 random.py, 从 shadow/use.py 里 import random:
random 来自 <工作目录>/shadow/random.py
random.randint(1, 100) = 4
同一个脚本, 但用 -P (脚本目录不进 sys.path) :
random 来自 <PREFIX>/lib/python3.14/random.py
random.randint(1, 100) = 9
文件本身叫 json.py, 里面 import json 再调用 dumps:
AttributeError: module 'json' has no attribute 'dumps' (consider renaming '<工作目录>/shadow/json.py' since it has the same name as the standard library module named 'json' and prevents importing that standard library module)
shadow/ 里放 os.py, 脚本里 import os: os 来自 <PREFIX>/lib/python3.14/os.py (os 启动时已经导入, 不会被遮蔽)脚本目录里放了一个自己写的 random.py,脚本 import random 拿到的是它——randint(1, 100) 永远返回 4;换成 -P,脚本目录不进 sys.path,才回到标准库的 random。文件叫 json.py、里面又 import json 时,得到 AttributeError,而且 3.14 的报错直接提示「可能因为同名遮蔽了标准库」。os 不会被遮蔽:因为它在启动时就已经被导入进 sys.modules,后面的 import 直接命中缓存,根本不会去搜索 sys.path。所以给自己的文件起名之前,别用标准库的名字。
第一次 import late_mod → ModuleNotFoundError: No module named 'late_mod' 文件刚创建后立刻 import → 成功, X = 1 importlib.invalidate_caches() 之后 → 成功, X = 1 连做 300 次「先 import 失败 → 立刻创建同名文件 → 再 import」, 第二次仍然找不到的有 0 次 sys.path_importer_cache 里 dyn 目录对应: FileFinder
运行时 sys.path.insert 一个目录就能从那里 import。路径查找器会缓存目录内容(sys.path_importer_cache 里对应 FileFinder),因此「先 import 失败、随后创建文件」理论上可能读到过期的目录列表,这时需要 importlib.invalidate_caches()。但我在这台机器上没能复现:连做 300 次都成功了——是否出问题取决于文件系统的时间戳精度,所以要不要调用它,别拿这一次实验当保证。
.pth 与 sys.path 的交互(见第 86 篇)、命名空间包、Windows 上的行为