文章目录
- 一个典型的插件系统包含以下组件: 协议定义:使用 typing.Protocol 定义插件必须实现的接口 插件发现:使用 pkgutil.iter_modules() 自动发现所有插件包 插件加载:使用 importlib.import_module() 动态导入插件模块 插件验证:运行时检查插件是否满足协议要求 插件执行:调用插件方法执行具体任务
- 首先定义插件协议: # jobs/base.py from typing import Protocol, runtime_checkable @runtime_checkable class JobProtocol(Protocol): “””插件协议定义””” def enabled(self) -> bool: “””判断插件是否启用””” … def run(self) -> bool: “””执行插件任务””” … @runtime_checkable 装饰器使得协议可以在运行时通过 isinstance() 进行检查。 写稿的时候想起来可以加个_runable()方法, 执行run()方法之前先检查是否满足可执行条件。 插件加载器实现: # main.py import logging import pkgutil import importlib from types import ModuleType from jobs.base import JobProtocol logger = logging.getLogger(__name__) def load_jobs(package: str = “jobs”) -> list[JobProtocol]: “””动态加载指定包下的所有任务插件””” loaded_jobs: list[JobProtocol] = [] # 导入目标包 pkg = importlib.import_module(package) # 遍历子包 for finder, name, ispkg in pkgutil.iter_modules(pkg.__path__, pkg.__name__ + “.”): # 只处理子包 if not ispkg: continue # 动态导入模块 module = importlib.import_module(name) # 获取工厂函数并创建实例 if hasattr(module, “job_factory”): job = module.job_factory() # 协议验证 if isinstance(job, JobProtocol) and job.enabled(): loaded_jobs.append(job) return loaded_jobs 插件实现示例: # jobs/jobs1/__init__.py from jobs.base import JobProtocol from .job import MyJob1 def job_factory() -> JobProtocol: return MyJob1() # jobs/jobs1/job.py class MyJob1: def enabled(self) -> bool: return True def run(self) -> bool: print(f”{self.__class__.__name__} is running”) return True 之所以每个插件放单独的package中,是想着如果插件功能复杂,单个文件的篇幅可能会极长,可以拆分到不同的文件中。每个插件也可以维护单独的配置加载方式。而且可以利用上 pkgutil 返回的 ispkg 。如果插件的功能简单,也可以写成单独的文件。
- project/├── main.py # 主程序入口├── jobs/│ ├── __init__.py # 包初始化文件│ ├── base.py # 协议定义│ ├── jobs1/ # 插件1│ │ ├── __init__.py # 导出 job_factory│ │ └── job.py # 具体实现│ ├── jobs2/ # 插件2│ │ ├── __init__.py│ │ └── job.py│ └── jobs3/ # 插件3(可禁用)│ ├── __init__.py│ └── job.py
- 2026-03-01 21:13:26 – INFO – 开始加载任务…2026-03-01 21:13:26 – INFO – 成功加载任务: jobs.jobs12026-03-01 21:13:26 – INFO – 成功加载任务: jobs.jobs22026-03-01 21:13:26 – INFO – 任务 jobs.jobs3 已禁用,跳过2026-03-01 21:13:26 – WARNING – 任务 jobs.jobs4 未实现 JobProtocol 协议(缺少 enabled 或 run 方法)2026-03-01 21:13:26 – INFO – 共加载 2 个任务2026-03-01 21:13:26 – INFO – 开始执行任务…MyJob1 is running2026-03-01 21:13:26 – INFO – 任务 MyJob1 执行完成,结果: TrueMyJob2 is running2026-03-01 21:13:26 – INFO – 任务 MyJob2 执行完成,结果: True2026-03-01 21:13:26 – INFO – 所有任务执行完毕
- 确保插件目录是规范的 Python 包: 每个插件包必须包含 __init__.py 文件 父包(jobs/)也应包含 __init__.py,确保 __path__ 属性正确设置 虽然 Python 3.3+ 支持命名空间包(无 __init__.py),但显式定义包结构更加健壮
- 动态加载过程中存在多种潜在的失败点,需要逐一处理: try: pkg = importlib.import_module(package) except ImportError as e: logger.error(f”导入包失败: {e}”) return [] for finder, name, ispkg in pkgutil.iter_modules(pkg.__path__, prefix): try: module = importlib.import_module(name) except Exception as e: logger.error(f”加载模块 {name} 失败: {e}”) continue if not hasattr(module, “job_factory”): continue try: job = module.job_factory() except Exception as e: logger.error(f”实例化插件 {name} 失败: {e}”) continue
- 生产环境中应使用 logging 模块: import logging logging.basicConfig( level=logging.INFO, format=”%(asctime)s – %(levelname)s – %(message)s”, ) logger = logging.getLogger(__name__) 这提供了日志级别控制、时间戳、输出重定向等关键能力。
- typing.Protocol 配合 @runtime_checkable 装饰器支持运行时类型检查: from typing import Protocol, runtime_checkable @runtime_checkable class JobProtocol(Protocol): def enabled(self) -> bool: … def run(self) -> bool: … # 检查实例是否满足协议 if isinstance(job, JobProtocol): job.run() 注意:运行时检查仅验证方法是否存在,不验证方法签名。如果参数类型不匹配,运行时仍会报错。
- 避免循环导入:插件模块不应导入主程序模块 延迟导入:插件内部的重量级依赖应在 run() 方法中导入,而非模块顶层 异常隔离:每个插件的执行应该相互独立,一个插件失败不应影响其他插件 def run_jobs(jobs: list[JobProtocol]) -> None: for job in jobs: try: job.run() except Exception as e: logger.error(f”任务执行失败: {e}”) # 继续执行其他任务
- 如果插件执行顺序很重要,可以考虑以下策略: 使用插件名称前缀排序(如 jobs/01_init/、jobs/02_process/) 在协议中添加 priority() 方法 在插件元数据中定义依赖关系
- iter_modules() 遍历文件系统,频繁调用可能影响性能 考虑在程序启动时一次性加载所有插件,后续使用缓存的插件列表 对于大量插件,可以考虑延迟加载(lazy loading)模式
- 动态加载代码存在潜在安全风险: 仅从可信路径加载插件 在沙箱环境中运行不受信任的插件 限制插件的文件系统和网络访问权限
目录
- pkgutil 简介
- 核心函数:iter_modules
- 实现动态插件系统
- 设计思路
- 代码实现
- 项目结构
- 运行结果示例
- 实际应用注意事项
- 包结构规范
- 错误处理策略
- 使用 logging 替代 print
- Protocol 运行时检查
- 插件隔离与依赖管理
- 插件顺序控制
- 性能考量
- 安全性考虑
- 补充
pkgutil 是 Python 标准库中的一个模块,提供了用于处理 Python 包的工具函数。它的核心功能之一是 iter_modules() 函数,能够动态遍历和发现指定包路径下的所有子模块和子包。这一特性使其成为实现动态插件系统的选择之一。(之前也介绍过借助__init_subclass__()在子类继承时动态注册插件)
与手动遍历文件系统或使用第三方库相比,pkgutil 具有以下优势:
- 标准库原生支持:无需引入额外依赖
- 跨平台兼容:统一处理不同操作系统的路径差异
- 支持命名空间包:能够正确处理 PEP 420 定义的命名空间包
- 与导入系统紧密集成:返回的模块名可直接用于
importlib.import_module()
iter_modules() 函数签名如下:
pkgutil.iter_modules(path=None, prefix='')
- path:要搜索的路径列表,通常使用包的
__path__属性 - prefix:返回的模块名前缀,常用于构建完整的模块导入路径
该函数返回一个迭代器,每个元素是一个三元组 (module_info_finder, name, ispkg):
module_info_finder:查找器对象(Python 3.6+ 为ModuleInfo实例)name:模块或包的名称(不含前缀)ispkg:布尔值,表示是否为包(含有__init__.py的目录)
一个典型的插件系统包含以下组件:
- 协议定义:使用
typing.Protocol定义插件必须实现的接口 - 插件发现:使用
pkgutil.iter_modules()自动发现所有插件包 - 插件加载:使用
importlib.import_module()动态导入插件模块 - 插件验证:运行时检查插件是否满足协议要求
- 插件执行:调用插件方法执行具体任务
首先定义插件协议:
# jobs/base.py
from typing import Protocol, runtime_checkable
@runtime_checkable
class JobProtocol(Protocol):
"""插件协议定义"""
def enabled(self) -> bool:
"""判断插件是否启用"""
...
def run(self) -> bool:
"""执行插件任务"""
...
@runtime_checkable 装饰器使得协议可以在运行时通过 isinstance() 进行检查。
写稿的时候想起来可以加个_runable()方法, 执行run()方法之前先检查是否满足可执行条件。
插件加载器实现:
# main.py
import logging
import pkgutil
import importlib
from types import ModuleType
from jobs.base import JobProtocol
logger = logging.getLogger(__name__)
def load_jobs(package: str = "jobs") -> list[JobProtocol]:
"""动态加载指定包下的所有任务插件"""
loaded_jobs: list[JobProtocol] = []
# 导入目标包
pkg = importlib.import_module(package)
# 遍历子包
for finder, name, ispkg in pkgutil.iter_modules(pkg.__path__, pkg.__name__ + "."):
# 只处理子包
if not ispkg:
continue
# 动态导入模块
module = importlib.import_module(name)
# 获取工厂函数并创建实例
if hasattr(module, "job_factory"):
job = module.job_factory()
# 协议验证
if isinstance(job, JobProtocol) and job.enabled():
loaded_jobs.append(job)
return loaded_jobs
插件实现示例:
# jobs/jobs1/__init__.py
from jobs.base import JobProtocol
from .job import MyJob1
def job_factory() -> JobProtocol:
return MyJob1()
# jobs/jobs1/job.py
class MyJob1:
def enabled(self) -> bool:
return True
def run(self) -> bool:
print(f"{self.__class__.__name__} is running")
return True
之所以每个插件放单独的package中,是想着如果插件功能复杂,单个文件的篇幅可能会极长,可以拆分到不同的文件中。每个插件也可以维护单独的配置加载方式。而且可以利用上 pkgutil 返回的 ispkg 。如果插件的功能简单,也可以写成单独的文件。
project/
├── main.py # 主程序入口
├── jobs/
│ ├── __init__.py # 包初始化文件
│ ├── base.py # 协议定义
│ ├── jobs1/ # 插件1
│ │ ├── __init__.py # 导出 job_factory
│ │ └── job.py # 具体实现
│ ├── jobs2/ # 插件2
│ │ ├── __init__.py
│ │ └── job.py
│ └── jobs3/ # 插件3(可禁用)
│ ├── __init__.py
│ └── job.py
project/
├── main.py # 主程序入口
├── jobs/
│ ├── __init__.py # 包初始化文件
│ ├── base.py # 协议定义
│ ├── jobs1/ # 插件1
│ │ ├── __init__.py # 导出 job_factory
│ │ └── job.py # 具体实现
│ ├── jobs2/ # 插件2
│ │ ├── __init__.py
│ │ └── job.py
│ └── jobs3/ # 插件3(可禁用)
│ ├── __init__.py
│ └── job.py
2026-03-01 21:13:26 – INFO – 开始加载任务…
2026-03-01 21:13:26 – INFO – 成功加载任务: jobs.jobs1
2026-03-01 21:13:26 – INFO – 成功加载任务: jobs.jobs2
2026-03-01 21:13:26 – INFO – 任务 jobs.jobs3 已禁用,跳过
2026-03-01 21:13:26 – WARNING – 任务 jobs.jobs4 未实现 JobProtocol 协议(缺少 enabled 或 run 方法)
2026-03-01 21:13:26 – INFO – 共加载 2 个任务
2026-03-01 21:13:26 – INFO – 开始执行任务…
MyJob1 is running
2026-03-01 21:13:26 – INFO – 任务 MyJob1 执行完成,结果: True
MyJob2 is running
2026-03-01 21:13:26 – INFO – 任务 MyJob2 执行完成,结果: True
2026-03-01 21:13:26 – INFO – 所有任务执行完毕
2026-03-01 21:13:26 – INFO – 开始加载任务…
2026-03-01 21:13:26 – INFO – 成功加载任务: jobs.jobs1
2026-03-01 21:13:26 – INFO – 成功加载任务: jobs.jobs2
2026-03-01 21:13:26 – INFO – 任务 jobs.jobs3 已禁用,跳过
2026-03-01 21:13:26 – WARNING – 任务 jobs.jobs4 未实现 JobProtocol 协议(缺少 enabled 或 run 方法)
2026-03-01 21:13:26 – INFO – 共加载 2 个任务
2026-03-01 21:13:26 – INFO – 开始执行任务…
MyJob1 is running
2026-03-01 21:13:26 – INFO – 任务 MyJob1 执行完成,结果: True
MyJob2 is running
2026-03-01 21:13:26 – INFO – 任务 MyJob2 执行完成,结果: True
2026-03-01 21:13:26 – INFO – 所有任务执行完毕
确保插件目录是规范的 Python 包:
- 每个插件包必须包含
__init__.py文件 - 父包(
jobs/)也应包含__init__.py,确保__path__属性正确设置 - 虽然 Python 3.3+ 支持命名空间包(无
__init__.py),但显式定义包结构更加健壮
动态加载过程中存在多种潜在的失败点,需要逐一处理:
try:
pkg = importlib.import_module(package)
except ImportError as e:
logger.error(f"导入包失败: {e}")
return []
for finder, name, ispkg in pkgutil.iter_modules(pkg.__path__, prefix):
try:
module = importlib.import_module(name)
except Exception as e:
logger.error(f"加载模块 {name} 失败: {e}")
continue
if not hasattr(module, "job_factory"):
continue
try:
job = module.job_factory()
except Exception as e:
logger.error(f"实例化插件 {name} 失败: {e}")
continue
生产环境中应使用 logging 模块:
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
这提供了日志级别控制、时间戳、输出重定向等关键能力。
typing.Protocol 配合 @runtime_checkable 装饰器支持运行时类型检查:
from typing import Protocol, runtime_checkable
@runtime_checkable
class JobProtocol(Protocol):
def enabled(self) -> bool: ...
def run(self) -> bool: ...
# 检查实例是否满足协议
if isinstance(job, JobProtocol):
job.run()
注意:运行时检查仅验证方法是否存在,不验证方法签名。如果参数类型不匹配,运行时仍会报错。
- 避免循环导入:插件模块不应导入主程序模块
- 延迟导入:插件内部的重量级依赖应在
run() 方法中导入,而非模块顶层
- 异常隔离:每个插件的执行应该相互独立,一个插件失败不应影响其他插件
def run_jobs(jobs: list[JobProtocol]) -> None:
for job in jobs:
try:
job.run()
except Exception as e:
logger.error(f"任务执行失败: {e}")
# 继续执行其他任务
run() 方法中导入,而非模块顶层
如果插件执行顺序很重要,可以考虑以下策略:
- 使用插件名称前缀排序(如
jobs/01_init/、jobs/02_process/) - 在协议中添加
priority()方法 - 在插件元数据中定义依赖关系
iter_modules() 遍历文件系统,频繁调用可能影响性能
- 考虑在程序启动时一次性加载所有插件,后续使用缓存的插件列表
- 对于大量插件,可以考虑延迟加载(lazy loading)模式
iter_modules() 遍历文件系统,频繁调用可能影响性能
动态加载代码存在潜在安全风险:
- 仅从可信路径加载插件
- 在沙箱环境中运行不受信任的插件
- 限制插件的文件系统和网络访问权限
代码示例
main.py
"""动态任务加载器
使用 pkgutil 模块动态发现和加载 jobs 包下的所有任务插件。
"""
import logging
import pkgutil
import importlib
from types import ModuleType
from jobs.base import JobProtocol
# 配置日志
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger(__name__)
def load_jobs(package: str = "jobs") -> list[JobProtocol]:
"""动态加载指定包下的所有任务插件。
遍历 package 下的所有子包,尝试导入每个子包并调用其 job_factory 函数
创建任务实例。只有实现了 JobProtocol 协议且 enabled() 返回 True 的
任务才会被执行。
Args:
package: 要扫描的包名,默认为 "jobs"。
Returns:
成功加载的任务实例列表。
"""
loaded_jobs: list[JobProtocol] = []
try:
pkg: ModuleType = importlib.import_module(package)
except ImportError as e:
logger.error(f"导入包 {package} 失败: {e}")
return loaded_jobs
# pkg.__path__ 可能是 None(当 package 是命名空间包但没有子包时)
if not hasattr(pkg, "__path__") or pkg.__path__ is None:
logger.warning(f"包 {package} 没有 __path__ 属性,无法遍历子模块")
return loaded_jobs
for finder, name, ispkg in pkgutil.iter_modules(pkg.__path__, pkg.__name__ + "."):
# 只处理子包,跳过模块文件
if not ispkg:
logger.debug(f"跳过模块 {name}(只加载子包)")
continue
try:
module: ModuleType = importlib.import_module(name)
except Exception as e:
logger.error(f"加载任务模块 {name} 失败: {e}")
continue
if not hasattr(module, "job_factory"):
logger.warning(f"模块 {name} 没有 job_factory 函数,跳过")
continue
try:
job: JobProtocol = module.job_factory()
# 使用 Protocol 的运行时检查功能验证协议实现
if not isinstance(job, JobProtocol):
logger.warning(
f"任务 {name} 未实现 JobProtocol 协议(缺少 enabled 或 run 方法)"
)
continue
if not job.enabled():
logger.info(f"任务 {name} 已禁用,跳过")
continue
loaded_jobs.append(job)
logger.info(f"成功加载任务: {name}")
except Exception as e:
logger.error(f"创建任务实例 {name} 失败: {e}")
continue
return loaded_jobs
def run_jobs(jobs: list[JobProtocol]) -> None:
"""执行所有任务。
Args:
jobs: 要执行的任务实例列表。
"""
for job in jobs:
try:
result = job.run()
logger.info(f"任务 {job.__class__.__name__} 执行完成,结果: {result}")
except Exception as e:
logger.error(f"任务 {job.__class__.__name__} 执行失败: {e}")
def main() -> None:
"""程序入口函数。"""
logger.info("开始加载任务...")
jobs = load_jobs()
logger.info(f"共加载 {len(jobs)} 个任务")
logger.info("开始执行任务...")
run_jobs(jobs)
logger.info("所有任务执行完毕")
if __name__ == "__main__":
main()
到此这篇关于Python使用pkgutil模块实现动态插件系统的文章就介绍到这了,更多相关Python pkgutil动态插件系统内容请搜索风君子博客以前的文章或继续浏览下面的相关文章希望大家以后多多支持风君子博客!
您可能感兴趣的文章:
- 基于Python编写一个基于插件架构的图片浏览器
- 使用Python快速生成chrome插件相关文件结构
- 使用Python创建自助抢单插件的完整步骤
- python实现一个通用的插件类
- Python中表格插件Tabulate的用法小结