Python3.5与3.7相对路径import_module差异及插件系统兼容问题
关于Python插件系统跨版本兼容的问题
场景与问题背景
我正在Python中实现一个插件系统,每个插件暴露完全相同的函数,可通过ID调用(如处理jpeg、bmp等不同格式的图片加载插件)。插件位于特定目录树中,尝试使用相对语法导入模块,但Python 3.5与Python 3.7的表现存在差异。
目录结构
root_dir | --main.py | --plugin_mod1 | | | --extend.py ... | --plugin_modN | | | --extend.py ...
main.py中的加载插件函数
# loads plugins with all the same interface def LoadPlugin(): global root_dir Extensions = glob.glob(root_dir+'/plugin_*') # ExtMods will store the retrieved module ExtMods = {} sys.path.insert(0, root_dir) for Ext in Extensions: if os.path.exists(Ext+'/extend.py'): # Python 3.7+ # tmp = importlib.import_module( # '.extend', os.path.basename(os.path.normpath(Ext))) # Python 3.5 sys.path.insert(0, Ext) tmp = importlib.import_module('extend') # bind some id to the found module ExtMods[tmp.PluginId()] = tmp return ExtMods
插件系统使用方式
... #loading all plugins found in root_dir PluginManager = LoadPlugin() ... # run DoSomething for plugin <some ID> PluginManager[<some ID>].DoSomething() ...
报错信息(Python 3.5 Ubuntu环境)
Traceback (most recent call last): File "<...>/main.py", line <xxx>, in <module> PluginManager = LoadPlugin() File "<...>/main.py", line <xxx>, in LoadPlugin '.extend', os.path.basename(os.path.normpath(Ext))) File "/usr/lib/python3.5/importlib/__init__.py", line 126, in import_module return _bootstrap._gcd_import(name[level:], package, level) File "<frozen importlib._bootstrap>", line 981, in _gcd_import File "<frozen importlib._bootstrap>", line 931, in _sanity_check SystemError: Parent module 'plugin_mod1' not loaded, cannot perform relative import
问题
- 为何两个Python版本的表现存在差异?
- 如何编写兼容至少Python 3.5的统一代码以实现需求?
- 是否可避免使用
sys.path.insert(0, <something>)(尤其是3.5版本中需临时添加每个插件路径的情况)?
回答
1. 版本差异原因
Python 3.6对导入机制做了优化:在使用importlib.import_module进行相对导入时,允许自动加载未预先导入的父模块;而Python 3.5及更早版本的导入逻辑更严格,要求父模块必须已经被加载到sys.modules中,否则会触发SystemError——因为旧版本会检查父模块的存在性,无法自动完成父模块的加载流程。
2. 兼容Python 3.5+的统一代码
提供两种可靠方案:
方案一:预加载父模块再导入子模块
先导入plugin_mod1这类父模块,再进行相对导入,适配旧版本的检查逻辑:
import importlib import glob import os import sys def LoadPlugin(): global root_dir Extensions = glob.glob(os.path.join(root_dir, 'plugin_*')) ExtMods = {} sys.path.insert(0, root_dir) for Ext in Extensions: extend_path = os.path.join(Ext, 'extend.py') if os.path.exists(extend_path): plugin_name = os.path.basename(os.path.normpath(Ext)) # 先加载父模块 importlib.import_module(plugin_name) # 再相对导入extend子模块 tmp = importlib.import_module('.extend', plugin_name) ExtMods[tmp.PluginId()] = tmp return ExtMods
方案二:临时添加路径并清理(更简单)
直接将每个插件目录临时加入sys.path,导入后立即移除,避免路径污染,兼容所有版本:
import importlib import glob import os import sys def LoadPlugin(): global root_dir Extensions = glob.glob(os.path.join(root_dir, 'plugin_*')) ExtMods = {} for Ext in Extensions: extend_path = os.path.join(Ext, 'extend.py') if os.path.exists(extend_path): # 临时添加插件目录到路径头部 sys.path.insert(0, Ext) try: tmp = importlib.import_module('extend') ExtMods[tmp.PluginId()] = tmp finally: # 导入完成后移除路径,避免后续导入冲突 sys.path.pop(0) return ExtMods
3. 避免修改sys.path的方案
可以使用importlib.util直接从文件路径加载模块,完全绕过常规导入路径检查,不需要修改sys.path,且兼容Python 3.5+(importlib.util从3.4开始支持):
import importlib.util import glob import os def LoadPlugin(): global root_dir Extensions = glob.glob(os.path.join(root_dir, 'plugin_*')) ExtMods = {} for Ext in Extensions: extend_path = os.path.join(Ext, 'extend.py') if os.path.exists(extend_path): # 创建模块规范 spec = importlib.util.spec_from_file_location('extend', extend_path) # 创建模块对象 tmp = importlib.util.module_from_spec(spec) # 执行模块代码 spec.loader.exec_module(tmp) # 绑定插件ID ExtMods[tmp.PluginId()] = tmp return ExtMods
这种方式更干净,不会对全局导入路径造成任何影响,是推荐的插件加载方案。
内容的提问来源于stack exchange,提问作者Oersted
相关产品推荐
相关产品推荐

