Python find_spec 探测子模块:还没导入它,为什么父包代码已经执行了

前天 4阅读

插件发现代码只是想判断 package.feature 是否存在,调用 find_spec 后却看到包的初始化日志。函数没有执行目标子模块,不代表整条查找路径完全没有导入行为。名称包含点号时,它需要导入父模块来获得子模块搜索路径,父包的初始化代码因此可能先运行。

如果父包初始化时连接外部服务、读取配置或注册全局回调,探测动作也可能触发这些行为。应把“规格能够找到”和“没有执行任何代码”分成两个要求。下面构造一个完全本地的包,用标记直接观察两层模块分别何时执行。

可直接运行的对照实验

保存为 demo.py,用 Python 3.12 运行。程序在专属临时目录里创建一个唯一包名,父包只设置 parent_runs,子模块设置 value。它短暂把目录加入 sys.path,结束时恢复搜索路径并清理自身模块记录,不修改任何已安装包。

Python find_spec 探测子模块:还没导入它,为什么父包代码已经执行了

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 中独立运行。

参考资料

Python 3.12 官方文档:importlib.util.find_spec

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。