Python find_spec 探测子模块:还没导入它,为什么父包代码已经执行了
插件发现代码只是想判断 package.feature 是否存在,调用 find_spec 后却看到包的初始化日志。函数没有执行目标子模块,不代表整条查找路径完全没有导入行为。名称包含点号时,它需要导入父模块来获得子模块搜索路径,父包的初始化代码因此可能先运行。
如果父包初始化时连接外部服务、读取配置或注册全局回调,探测动作也可能触发这些行为。应把“规格能够找到”和“没有执行任何代码”分成两个要求。下面构造一个完全本地的包,用标记直接观察两层模块分别何时执行。
可直接运行的对照实验
保存为 demo.py,用 Python 3.12 运行。程序在专属临时目录里创建一个唯一包名,父包只设置 parent_runs,子模块设置 value。它短暂把目录加入 sys.path,结束时恢复搜索路径并清理自身模块记录,不修改任何已安装包。
AI概念插图:为了查看内部小盒子的位置,外层盒子已经先被打开。图片不是运行截图,也不表示实测性能。
import importlib
import importlib.util
import sys
import uuid
from pathlib import Path
from tempfile import TemporaryDirectory
name = "probe_pkg_" + uuid.uuid4().hex
with TemporaryDirectory(prefix="find-spec-demo-") as folder:
package = Path(folder) / name
package.mkdir()
(package / "__init__.py").write_text("parent_runs = 1\n", encoding="utf-8")
(package / "feature.py").write_text("value = 42\n", encoding="utf-8")
sys.path.insert(0, folder)
try:
importlib.invalidate_caches()
assert importlib.util.find_spec(name) is not None
assert name not in sys.modules
print("top-level lookup: package not imported")
child = name + ".feature"
assert importlib.util.find_spec(child) is not None
assert name in sys.modules and child not in sys.modules
assert sys.modules[name].parent_runs == 1
print("parent imported, child pending:",
sys.modules[name].parent_runs, child not in sys.modules)
loaded = importlib.import_module(child)
assert loaded.value == 42
assert importlib.util.find_spec(name + ".missing") is None
print("child value:", loaded.value)
finally:
sys.path.remove(folder)
for module_name in list(sys.modules):
if module_name == name or module_name.startswith(name + "."):
del sys.modules[module_name]
importlib.invalidate_caches()父包被导入,子模块仍未执行
查找顶层包的规格后,包名尚未出现在 sys.modules;查找点号子模块后,父包已经存在且 parent_runs 等于一,子模块仍不在模块缓存。第二行因此显示 parent imported, child pending: 1 True。这个对照把父包执行与子模块执行明确分开。
随后正式 import_module,子模块才进入缓存并给出 value=42。父包不会因为这次正常子模块导入又从头运行一遍;本例没有重新加载或手工篡改缓存。若在交互式环境反复运行片段而不清理模块状态,第二轮现象可能不同,所以完整脚本更适合复现。
找到规格不是导入成功证明
规格描述加载器、来源与搜索相关信息,却不会提前证明子模块执行时不报错。子模块里缺少依赖、存在语法问题或主动抛异常,都可能在正式导入时才暴露。插件检测若只记一个“存在”布尔值,应避免将它展示成“已加载可用”。
例子还查找父包内一个不存在的子模块并得到 None。这与父包本身导入失败是不同分支;父包初始化异常可能直接传播出来。业务代码不应把所有异常吞掉后都解释成未安装,否则用户看不到真正的配置、依赖或执行错误。
发现协议需要与加载行为配套
如果只要读取发行包元数据,可以先考虑专门的元数据接口,并明确发行包名称与导入名称未必相同。若需要插件能力清单,也可以让插件提供独立声明,减少启动探测时必须执行的初始化工作。具体方案取决于系统如何安装和注册插件。
这不是对任意导入钩子的安全隔离方案。find_spec 会参与 Python 的导入查找机制,自定义查找器本身也可以执行代码;例子中的顶层查找没有执行包体,只证明普通临时文件包的这条路径。不要把这个结果扩大成所有环境都无副作用。
验收时先用干净进程记录 sys.modules,再分别执行顶层探测、子模块探测与正式导入,同时留意外部可见动作。若父包初始化很重,优先考虑把昂贵动作移动到明确调用阶段。这样插件发现会更可解释,调用者也能知道何时真正开始使用资源。
资料核对日期:2026年10月2日(北京时间)。代码在 CPython 3.12.14 中独立运行。


