bigywater.tech
Python · 87

sys.path:模块搜索路径是怎么生成的

import x 找不到、或者找到的不是你想要的那个 x,几乎总是 sys.path 的问题。这篇不背规则,而是用不同的启动方式、一个故意同名的文件,把 sys.path 的来源和搜索顺序真实跑出来。

sys.path[0]
脚本运行时是脚本目录(符号链接会被解析),-c 时是空字符串
randint(1, 100) = 4
同目录下的 random.py 遮蔽了标准库
os 不受影响
启动时已导入的模块不会被同名文件遮蔽

验证一:sys.path[0] 取决于怎么启动

实测 · 本机执行输出
  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 条目。

验证二:完整的 sys.path 从哪来

实测 · 本机执行输出
  (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。所以给自己的文件起名之前,别用标准库的名字。

验证四:运行时改 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 次都成功了——是否出问题取决于文件系统的时间戳精度,所以要不要调用它,别拿这一次实验当保证。

分步演示:同一个 import random,搜索路径上谁先命中

每一项的结果都来自对该条目单独调用 PathFinder.find_spec
sys.path 的搜索与遮蔽
当前检查的条目 / 命中的条目

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7(Homebrew),路径里的 <PREFIX> 表示解释器安装前缀,<工作目录> 是临时目录
    • 没有验证:虚拟环境里的 sys.path 差异、.pth 与 sys.path 的交互(见第 86 篇)、命名空间包、Windows 上的行为
    京ICP备2026067502号-1京公网安备11010502063487号