Python アプリを作っていると、動的にクラスをインポートしたいニーズがときどきあります。
覚えた機能について今後も使う可能性があれば、その時になってあれこれ調べ直さなくて済むように、簡単なサンプルを作成して本ブログに記事にまとめています。動的にクラスをインポートについても、何年か前に記事を書いていました [1]。しかし、読み直してみると、今ひとつよく解らなかったので、一般的な用途に合うようにサンプルを作り直しました。
下記の環境で動作確認をしています。
|
Fedora Linux 44 KDE Plasma Desktop x86_64 |
|
| Python | 3.14.5 | |
| PySide6 | 6.11.1 | |
プラグイン用サンプル
まず、動作確認用にプラグイン用サンプルを用意します。プロジェクト内に plugins というディレクトリを作り、テンプレート用の抽象クラスを記述した abstract.py(PluginTemplate クラス)と、このクラスを継承して実装した simple_1.py と simple_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.py と simple_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 メソッドを実行します。
simple 1 simple 2
参考サイト
- bitWalk's: プラグイン・ウィジェットを扱う ~ PySide6 ~ [2023-02-07]
- importlib --- import の実装 — Python ドキュメント
- inspect --- 活動中のオブジェクトを調査する — Python ドキュメント
- pkgutil --- パッケージ拡張ユーティリティ — Python ドキュメント
にほんブログ村
#オープンソース




0 件のコメント:
コメントを投稿