2026-06-20

モジュールを動的にインポート 〜 Python 〜

Python アプリを作っていると、動的にクラスをインポートしたいニーズがときどきあります。

覚えた機能について今後も使う可能性があれば、その時になってあれこれ調べ直さなくて済むように、簡単なサンプルを作成して本ブログに記事にまとめています。動的にクラスをインポートについても、何年か前に記事を書いていました [1]。しかし、読み直してみると、今ひとつよく解らなかったので、一般的な用途に合うようにサンプルを作り直しました。

下記の環境で動作確認をしています。

Fedora Linux 44
KDE Plasma Desktop x86_64
Python 3.14.5
PySide6 6.11.1

プラグイン用サンプル

まず、動作確認用にプラグイン用サンプルを用意します。プロジェクト内に plugins というディレクトリを作り、テンプレート用の抽象クラスを記述した abstract.pyPluginTemplate クラス)と、このクラスを継承して実装した simple_1.pysimple_2.py を用意します。

plugins/
├ abstract.py
├ simple_1.py
└ simple_2.py
from abc import ABC, abstractmethod


class PluginTemplate(ABC):
    NAME = "template"

    @abstractmethod
    def run(self) -> None: ...
from plugins.abstract import PluginTemplate


class Plugin(PluginTemplate):
    NAME = "simple 1"

    def run(self) -> None:
        print(self.NAME)
from plugins.abstract import PluginTemplate


class Plugin(PluginTemplate):
    NAME = "simple 2"

    def run(self) -> None:
        print(self.NAME)

PluginTemplate クラスを継承して実装した simple_1.pysimple_2.py のクラス名は、ひとまず Plugin に統一しています。

クラス一覧をインポートする関数

指定したパス、パッケージ名、継承したクラス名に合致するモジュールをロードする関数 load_plugins です。

import importlib
import inspect
import pkgutil


def load_plugins(
        path_plugin: str,
        package_name: str,
        plugin_base_class: type
) -> dict[str, type]:
    # NOTE:
    # 現在はプロジェクト配下のパッケージのみを対象としているため、
    # importlib.import_module() を利用している。
    #
    # 将来的にユーザーが任意のディレクトリへ配置した外部プラグインを
    # 読み込む場合は、sys.path の追加、または
    # importlib.util.spec_from_file_location() を利用した
    # ファイルパスベースのロード方式を検討すること。
    dict_plugin = {}
    for _, module_name, _ in pkgutil.iter_modules([path_plugin]):
        module = importlib.import_module(f"{package_name}.{module_name}")
        for _, cls in inspect.getmembers(module, inspect.isclass):
            if issubclass(cls, plugin_base_class) and cls is not plugin_base_class:
                dict_plugin[cls.NAME] = cls
    return dict_plugin

引数 plugin_base_class に指定した抽象クラス(この例では PluginTemplate)を継承したクラス cls のみを辞書に cls.NAME をキーに登録して返す関数です。

テスト用 GUI サンプル

動的にクラスをインポートするプラグインの機能を確認する PySide6 の GUI サンプルです。

import sys

from PySide6.QtGui import QAction
from PySide6.QtWidgets import (
    QApplication,
    QComboBox,
    QMainWindow,
    QStyle,
    QToolBar,
)

from load_plugins import load_plugins
from plugins.abstract import PluginTemplate


class Example(QMainWindow):
    def __init__(self):
        super().__init__()

        # プラグインの一覧を取得
        plugins: dict[str, type] = load_plugins(
            path_plugin="./plugins",
            package_name="plugins",
            plugin_base_class=PluginTemplate
        )

        self.setWindowTitle("Plugin Sample")

        toolbar = QToolBar()
        self.addToolBar(toolbar)

        self.combo = combo = QComboBox()
        for key in sorted(plugins.keys()):
            cls = plugins[key]
            combo.addItem(key, cls)
        toolbar.addWidget(combo)

        icon = self.style().standardIcon(
            QStyle.StandardPixmap.SP_MediaPlay
        )
        action_play = QAction(self)
        action_play.setIcon(icon)
        action_play.triggered.connect(self.on_play)
        toolbar.addAction(action_play)

    def on_play(self):
        cls = self.combo.currentData()
        obj = cls()
        obj.run()


def main():
    app = QApplication(sys.argv)
    ex = Example()
    ex.show()
    sys.exit(app.exec())


if __name__ == '__main__':
    main()

サンプルはコンボボックスに動的に読み込んだクラスの NAME の一覧を表示しています。隣の ▶ ボタンをクリックすると、コンボボックスに表示されている NAME に対応するクラスのインスタンスを生成して、run メソッドを実行します。

qt_plugin_sample.py の実行例
simple 1
simple 2

参考サイト

  1. bitWalk's: プラグイン・ウィジェットを扱う ~ PySide6 ~ [2023-02-07]
  2. importlib --- import の実装 — Python ドキュメント
  3. inspect --- 活動中のオブジェクトを調査する — Python ドキュメント
  4. pkgutil --- パッケージ拡張ユーティリティ — Python ドキュメント

 

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



このエントリーをはてなブックマークに追加