惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

J
Java Code Geeks
Last Week in AI
Last Week in AI
T
Tailwind CSS Blog
WordPress大学
WordPress大学
B
Blog RSS Feed
T
The Blog of Author Tim Ferriss
F
Fortinet All Blogs
aimingoo的专栏
aimingoo的专栏
MongoDB | Blog
MongoDB | Blog
博客园 - Franky
C
Check Point Blog
P
Proofpoint News Feed
H
Help Net Security
月光博客
月光博客
博客园_首页
Stack Overflow Blog
Stack Overflow Blog
博客园 - 三生石上(FineUI控件)
Martin Fowler
Martin Fowler
Recent Announcements
Recent Announcements
人人都是产品经理
人人都是产品经理
U
Unit 42
美团技术团队
I
InfoQ
A
About on SuperTechFans

皓子的小站

网站字体应用之坑——font-family 篇 糟糕的 meta name="theme-color" How to Automatically Track Newly Supported ESLint Rules in Oxlint How to Gradually Migrate from ESLint to Oxlint (Without Breaking Everything) 静态资源预压缩:零运行时开销,极致节省带宽 CVE-2025-70886 Proof of Concept (PoC) | A Script to Crash Halo CMS Comment Backend 自定义网页鼠标指针——一段曲折的旅程 CVE-2025-70886 漏洞复现/PoC | 一个脚本让 Halo CMS 评论后台瘫痪 2025 年终总结 & 博客两周年 老用户专享!已有 Halo 专业版授权可免费升级商业版 自动追踪 Oxlint 对 ESLint 规则的新增支持 Halo 贡献者证书与实体周边盲盒开箱 博客评论系统指南 CDN 回源跟随配置导致登录异常问题排查 SCDN 免费赞助计划:助力高质量博客&博客圈 锐捷校园网:网络共享与带宽叠加方案(哈理工案例) ESLint 到 Oxlint 渐进式迁移快速上手指南 Fixing Vite Breaking Inline JS & CSS in Thymeleaf Templates 解决 Vite 破坏 Thymeleaf 模板内联 JS & CSS 的方法 我的博客,为什么是月更? 博客俱乐部一周年纪念品开箱 网站字体加载之坑——format 篇 经历 1000000000 次 DDoS 请求攻击后,我总结了三条经验 题解分享:[AtCoder Beginner Contest 414 E] Count A%B=C 题解分享:[蓝桥杯 2025 国 Python A] 杨辉三角 P12876 FiF口语训练破解刷分教程(适用于 Windows) 不要与蠢人辩论 小心网络地雷·续篇 当心网络组织“开往”·续篇 当心网络组织“开往”
如何在 Python 实现程&...
HowieHz · 2024-01-02 · via 皓子的小站

内容概要:如何在python实现程序加载插件(动态加载模块文件,插件化开发)

初发布于如何在python实现程序加载插件(动态加载模块文件,插件化开发) - 知乎 (zhihu.com) 发布于 2022.06.19 10:14
后发布于项目FSMargoo/one-day-one-point: 每天一个技术点 (github.com) 发布于 2023.7.15

前言

PyCharm,vscode 都可以通过添加插件拓展软件功能
Minecraft,Terraria,Don't Starve 都可以通过添加mod拓展游戏玩法

Python 模块的格式就是 .py(或者 .pyd),这对于我们来说无疑是一种便利。那么究竟如何让 python 程序有扫描并加载插件(额外模块)的功能呢

本文中 “插件”,“动态加载的模块”,“额外模块” 同意

我们将探究以下问题

  1. 动态加载模块文件
  2. 导入指定位置的模块(相对位置或绝对位置)
  3. 让程序找到要加载的模块

一.如何动态加载模块文件

1.1 使用 import 关键字(无法实现)

importfrom 关键字加载模块,是我们最常用的一种方式,但是关键字后面的模块名不能是变量,是已经被确定下来的常量
用诸如 pyinstaller 或者 nuitka 工具打包的话,也是不允许这些在关键字后面的模块名空着

1.2 使用 import 函数(官方不建议使用)

__import__函数(python 官方文档)
import 实际上是调用这个内置函数,不建议使用,官方建议使用importlib.import_module

1.3 使用 importlib 模块

importlib --- import 的实现(python 官方文档)
importlib.import_module(name, package=None) 简单好用,两个参数,一个指定包,一个指定模块
指定 package 之后,name 可以使用诸如 ".aaa.aaa" 的方式相对导入
实测程序在使用 nuitka 和 pyinstaller 打包后依然可以正常调用插件
值得注意的是,如果要使用相对路径,要注意执行二进制文件 (如.exe 文件) 的位置是相对路径的出发点

二.导入指定位置的模块

目录结构

src
├package
│├module1.py
│└module2.py
├inside
│└__main__2.py
└__main__1.py

2.1 导入同层或者下层目录的模块

__main__1.py 是入口文件

import importlib
# 进行了返回值的类型标注,变量名随意

# module1 在同层的叫做package的包里面,可以直接导入
module1 = importlib.import_module("package.module1")

# module2 在同层的叫做package的包里面,可以直接导入
module2 = importlib.import_module(".module2", package="package")

# __main__2 在同层的叫做inside的包里面,可以直接导入
__main__2 = importlib.import_module("inside.__main__2")

2.2 导入上层位置的模块

__main__2.py 作为入口文件

2.2.1 from import 关键字(无法解决)

如果在 __main__2.py 使用

from ..package import module1

会报错 ImportError: attempted relative import with no known parent package
因为只有在一个包中才能使用相对导入,__main__2.py 是主程序而不是包, 这样使用是不合法的

2.2.2 sys.path

sys.path 就像 Windows 的系统变量一样,是 Python 解释器搜索包的位置
所以我们只需要在 sys.path 这个列表里面加入上层目录就可以使用importlib.import_module 加载模块了,这个上层目录可以使用 ".." ,也可以使用绝对路径
(sys.path 默认有 "." 这就是为什么导入同层的包无需添加)
以下是示例

import sys

# 相对路径
sys.path.append("..")

# 绝对路径
实际测试上面那种写法在打包后可能失效,所以推荐使用下面这种写法
sys.path.append(os.path.join(sys.path[0], ".."))
# sys.path[0] 是入口文件的运行位置
2.2.3 示例

__main__2.py 是入口文件

import importlib

# 将上层目录添加到加载路径内
sys.path.append("..")

module1 = importlib.import_module("package.module1")
module2 = importlib.import_module(".module2", package="package")
__main__1 = importlib.import_module("__main__1")

三.让程序找到要加载的插件

方法多种多样,管理文件的模块 Python 已经有内置模块实现了,如 os.pathpathlib

这里就可以做你的插件规范了,
你可以规定你的插件必须是 .zip 压缩包,里面放着json格式的元数据和 .py 后缀的插件主体文件

最基础的加载一个插件的流程为

  1. 找到放着代码的插件主体文件
  2. 将路径转换为包名和模块名的形式
  3. 传入importlib.import_module(name, package=None)

本文为了方便实现,做一个简单的插件规范:只需要放 .py 后缀的插件主体文件到存放插件的目录就是一个合法的插件
对应流程为

  1. 扫描指定插件文件夹下的所有文件
  2. 过滤出我们想要的 .py 后缀的插件主体文件
  3. 转换为可以传入 importlib.import_module() 参数的形式
import os

# 第一步(读取)
# 读取path目录下的全部的文件和文件夹
things_in_plugin_dir: list = os.listdir(path)

# 第二步(过滤)和第三步(转换)
def pick_module(name):
  if name.endswith(plugin_suffix):  # 检查文件名后缀是否是.py  -> 过滤
    return name.split["."](0)  # 后缀是.py 就提取文件名  -> 转换
  else:
    return ""  # 后缀不是.py 就把这项置空

# 挑选出.py 为后缀的文件
files_in_plugin_dir: list = map(pick_module, things_in_plugin_dir)

# 这里也给出使用三元表达式和 lambda 的写法
def pick_module2(name):
  return map(
    lambda file_name: file_name.split(".")[0]  # 后缀是.py 就提取文件名
    if file_name.endswith(plugin_suffix)  # 检查文件名后缀是否是.py
    else "",  # 后缀不是.py 就把这项置空
    things_in_plugin_dir,
    )

# files_in_plugin_dir: list = pick_module2(path)

# 去除列表中的空值
files_in_plugin_dir: list = list(filter(None, files_in_plugin_dir))

最终示例

该示例实现了扫描插件,加载插件
插件规范: .py 后缀的插件主体文件放到..src.plugins就是一个合法的插件

目录结构

src
├plugins
│├module1.py
│└module1.py
└main_program
 └__main__.py

其中 module1.pymodule2.py 是两个插件

module1.py 文件中的内容

# plugins/module1.py
version = "v1.0.0"

def test():
  print("this is a test func")

class testClass():
  def __init__():
    print("this is a test class")

  def test():
    print("this is a test func in class")

main.py 为入口文件

# src/main.py
import os
import importlib
import traceback
from types import ModuleType

loaded_plugins: dict[ModuleType] = {} # 这个字典用于保存加载好的插件对象

# 将上层目录的 package 文件夹添加到 sys.path 中,可以直接通过文件名导入
sys.path.append(os.path.join(sys.path[0], "..", "package"))

# 常量设置
plugin_suffix = "py" # 插件后缀为 py
path = os.path.join("..", "package") # 设置插件文件夹


# 读取该目录下的的文件和文件夹
things_in_plugin_dir = os.listdir(path)

def pick_module(name):
  if name.endswith(plugin_suffix): # 检查文件名后缀是否是.py
    return name.split["."](0) # 后缀是.py 就提取文件名
  else:
    return "" # 后缀不是.py 就把这项置空
# 挑选出.py 为后缀的文件
files_in_plugin_dir: list = map(pick_module, things_in_plugin_dir) 

# 去除列表中的空值
files_in_plugin_dir: list = list(filter(None, files_in_plugin_dir))


# 加载插件
for name in files_in_plugin_dir:
  try:
    loaded_plugins[name] = importlib.import_module(f"{name}")
    # 插件缺少依赖(ImportError 包括 ModuleNotFoundError)
  except ModuleNotFoundError:
    traceback.print_exc() # 输出报错信息用于排错
    continue
  except ImportError:
    traceback.print_exc()
    continue
  except Exception as e:
    print(f"A problem:{e}")
    traceback.print_exc()
    continue

# 这时你就得到了一个装着插件对象的字典
# 只需要使用 loaded_plugins[文件名] 就能调用对应插件


# 测试加载的插件
# 比如此时我要使用 module1 的 test 方法,只需要
loaded_plugins["module1"].test()
# 预期输出:this is a test func

# 比如此时我要实例化 module1 的 testMain 类,只需要
instance = loaded_plugins["module1"].testClass()
# 预期输出:this is a test class

instance.test()
# 预期输出:this is a test func in class

# 比如此时我要输出 module1 的 version 属性,只需要
print(loaded_plugins["module1"].version)
# 预期输出:v1.0.0

补充

具有插件加载功能的库/软件:
HowieHz/hpyculator: high extensibility calculator base on python (github.com)
该项目中的实现plugin_manager.py at main · HowieHz/hpyculator (github.com)

Fallen-Breath/MCDReforged: A rewritten version of MCDaemon, a python script to control your Minecraft server (github.com)

nonebot/nonebot2: 跨平台 Python 异步聊天机器人框架 / Asynchronous multi-platform chatbot framework written in Python (github.com)