python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python pkgutil动态插件系统

Python使用pkgutil模块实现动态插件系统

作者:花酒锄作田

pkgutil是Python标准库中的一个模块,提供了用于处理Python包的工具函数,下面我们就来看看如何借助pkgutil模块实现简单的动态插件系统吧

pkgutil 简介

pkgutil 是 Python 标准库中的一个模块,提供了用于处理 Python 包的工具函数。它的核心功能之一是 iter_modules() 函数,能够动态遍历和发现指定包路径下的所有子模块和子包。这一特性使其成为实现动态插件系统的选择之一。(之前也介绍过借助__init_subclass__()在子类继承时动态注册插件)

与手动遍历文件系统或使用第三方库相比,pkgutil 具有以下优势:

核心函数:iter_modules

iter_modules() 函数签名如下:

pkgutil.iter_modules(path=None, prefix='')

该函数返回一个迭代器,每个元素是一个三元组 (module_info_finder, name, ispkg)

实现动态插件系统

设计思路

一个典型的插件系统包含以下组件:

代码实现

首先定义插件协议:

# 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.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 包:

错误处理策略

动态加载过程中存在多种潜在的失败点,需要逐一处理:

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 替代 print

生产环境中应使用 logging 模块:

import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)

这提供了日志级别控制、时间戳、输出重定向等关键能力。

Protocol 运行时检查

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()

注意:运行时检查仅验证方法是否存在,不验证方法签名。如果参数类型不匹配,运行时仍会报错。

插件隔离与依赖管理

def run_jobs(jobs: list[JobProtocol]) -> None:
    for job in jobs:
        try:
            job.run()
        except Exception as e:
            logger.error(f"任务执行失败: {e}")
            # 继续执行其他任务

插件顺序控制

如果插件执行顺序很重要,可以考虑以下策略:

性能考量

安全性考虑

动态加载代码存在潜在安全风险:

补充

代码示例

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动态插件系统内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文