pkgutil不能直接构建健壮插件架构,因其仅负责发现模块,不处理判断、加载和使用;需配合importlib.import_module、路径拼接及错误隔离,否则易漏模块或崩溃。

pkgutil 可以用来做插件发现,但**不能直接构建健壮的插件架构**——它只负责“找”,不负责“判”“载”“用”。真正落地时,必须搭配 importlib.import_module、显式路径拼接和错误隔离,否则极易漏模块、炸进程、或加载失败却不报错。
为什么 pkgutil.iter_modules() 只扫一层?
pkgutil.iter_modules(path) 本质是读取指定 path 下的文件夹内容,只检查该目录一级内的 .py 文件和含 __init__.py 的子目录。它不会自动钻进 utils/network/ 去看里面有没有 client.py。
- 常见错误现象:
myproject.plugins下有auth/和auth/jwt.py,但iter_modules(myproject.plugins.__path__)只返回auth(子包名),不返回jwt - 判断是否为子包:看
ModuleInfo.ispkg字段,True才代表是包,才值得递归 - 递归入口必须是子包的
__path__,不是字符串路径 —— 错用["./plugins/auth"]会绕过 Python 导入系统,丢失命名空间和缓存
pkgutil.walk_packages() 看似递归,但要注意三个坑
walk_packages() 确实递归遍历,但它返回的是 (finder, name, ispkg) 元组,其中 name 是相对名(如 "auth.jwt"),不是完整模块路径;且它不校验模块能否被导入。
- 必须手动拼出完整模块名:
f"{parent_package}.{info.name}",比如起始包是"myproject.plugins",当前name是"auth.jwt",则完整名为"myproject.plugins.auth.jwt" - 不能直接
importlib.import_module(name)—— 因为name是相对路径,没上下文会报ModuleNotFoundError - 遇到语法错误、缺失依赖、或未安装的第三方库时,
ImportError会中断整个遍历;必须用try/except ImportError包裹单个导入
如何安全地加载并验证每个插件模块?
找到模块名只是第一步。真正可用的插件,通常要满足接口契约(比如继承某个基类、实现特定方法)。这一步 pkgutil 完全不管,得你亲手做。
- 导入后,用
hasattr(module, "execute")或isinstance(getattr(module, "PluginClass", None), type)做基本存在性检查 - 避免用
dir(module)遍历所有属性 —— 可能触发模块内副作用(比如初始化数据库连接) - 推荐模式:定义一个协议(如
class PluginProtocol(Protocol): def execute(self) -> None: ...),然后用typing.runtime_checkable+isinstance(obj, PluginProtocol)做轻量校验 - 不要在导入时执行插件逻辑(比如调
module.start())—— 应该由主程序统一调度,否则难以控制启动顺序和生命周期
为什么生产环境慎用 pkgutil 做插件发现?
它基于文件系统扫描,天然不兼容现代 Python 的一些关键机制。
- 对 namespace package(PEP 420,无
__init__.py的包)完全不可见 - 无法识别通过
zipimport加载的包(比如打包成 .exe 后的 PyInstaller 场景) - 不感知
sys.meta_path上自定义的 finder(如 import-time hook、远程模块加载器) - 编辑中未保存的
.py文件、临时生成的模块,也不会被纳入扫描结果
如果项目已用 setuptools 或 importlib.metadata,优先考虑 entry_points;若需极致轻量,os.listdir() + importlib.util.spec_from_file_location() 反而更可控、更透明。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











