You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

问题

  1. 为何两个Python版本的表现存在差异?
  2. 如何编写兼容至少Python 3.5的统一代码以实现需求?
  3. 是否可避免使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 16:22:01