Pythonで動的にクラスロードして実行してみた
今、個人的な開発でオープンデータを取集して解析するコード書いています。
オープンデータで公開しているファイルの書式がバラバラでparserを多く書くことになりそうなんで、pythonで動的なクラスロードの仕組みを調べました。
(parserを追加するごとに呼び出しもとをコードを修正するのは大変なので)
実現したいこと
- parserは所定のディレクトリに配置されたら、動的に読み込まれる対象となる
- 読み込まれるモジュールはparserの基底クラスを継承している
- 基底クラスの特定メソッドを呼び出して処理を実行する
Pythonで動的にクラスロードして実行
前提条件
以下の環境で試しました。
抽象クラスとabstract method
検索してみると抽象基底クラス(Abstract Base Class:ABC)というものが準備されているとのことです。
abc --- 抽象基底クラス — Python 3.7.6 ドキュメント
以下の構成でファイルを作りました。
- main.py:実行モジュールです
- parserディレクトリ
抽象基底クラス
# base_parser.py from abc import ABCMeta, abstractmethod class BaseParser(metaclass=ABCMeta): @abstractmethod def parse(self): pass
まず、抽象基底クラスはABCMetaを継承します。
具象クラスに実装させたいメソッドにはabstractmethodデコレータを追加します。このメソッドに実装を定義できないので、passを定義します。
これでBaseParserクラスを継承したクラスは必ずparseメソッドを実装することになります。
具象クラス
試しにbunkyoku_parser.pyにabstractmethodを実装しない状態でインスタンス化するとエラーになります。
# bunkyoku_parser.py from parser.base_parser import BaseParser class BunkyokuParser(BaseParser): def echo(self): print("echo bunkyoku")
echoメソッドではなく、parseメソッドに変えてからBunkyokuParserクラスをインスタンス化するとエラーにならず、正常に実行できました。
# bunkyoku_parser.py from parser.base_parser import BaseParser class BunkyokuParser(BaseParser): def parse(self): print("echo bunkyoku")
importlibで動的にモジュールを読み込む
main.pyに動的にモジュールを呼び出すコードを定義します。
# main.py import os import importlib def parse(): package_name = 'parser' parser_dir = os.listdir(package_name) skip_class_name = ['__init__.py', 'base_parser.py'] for file_name in parser_dir: if file_name.endswith('py') and \ file_name not in skip_class_name: module_name = os.path.splitext(file_name)[0] classpath = package_name + '.' + module_name parser = importlib.import_module(classpath) if __name__ == '__main__': parse()
parserパッケージ配下の一覧をos.listdir(usrsrc) で取得します。
skip_class_nameにはクラスロード対象外のファイル名を定義しておきます。
package_name = 'parser' parser_dir = os.listdir(package_name) skip_class_name = ['__init__.py', 'base_parser.py']
上記で取得した一覧から「拡張子がpy」かつ「skip_class_nameに含まれていないファイル」を処理対象とします。
for file_name in parser_dir: if file_name.endswith('py') and \ file_name not in skip_class_name:
処理対象のファイル名から拡張子を除いたものをモジュール名としてclasspathを生成します。(例:bunkyoku_parser.py → parser.bunkyoku_parser)
生成したclasspathをもとにimportlib.import_moduleでモジュールを読み込みます。
module_name = os.path.splitext(file_name)[0] classpath = package_name + '.' + module_name parser = importlib.import_module(classpath)
これでモジュールを読み込むことができました。
動的に読み込んだモジュールから特定のメソッドを呼び出す
モジュールを読み込むことが出来たので、モジュールの中に含まれているクラスを呼び出して実行します。
読み込んだモジュールの中に含まれるクラス名は読み込んだ時点で分からないので、inspect経由でオブジェクトの情報を取得します。
inspect --- 活動中のオブジェクトの情報を取得する — Python 3.7.6 ドキュメント
parser = importlib.import_module(classpath) clazz_list = inspect.getmembers(parser, inspect.isclass) for clazz in clazz_list: if clazz[0] != 'BaseParser': instance = clazz[1]() instance.parse()
import_moduleで読み込んだモジュールからクラス一覧を取得します。
parser = importlib.import_module(classpath) clazz_list = inspect.getmembers(parser, inspect.isclass)
取得したクラス一覧から抽象基底クラスであるBaseParserは除外して、それ以外のクラスのインスタンスを生成します。
ここで登場するクラスはBaseParserクラスを継承しているので、必ずparseメソッドが存在します。
最後にinstance.parse()を実行することで動的にロードしたクラスの特定メソッドを実行することが出来ます。
for clazz in clazz_list: if clazz[0] != 'BaseParser': instance = clazz[1]() instance.parse()
実行した結果はこちらです。
各モジュールに含まれるBaseParserクラスを継承したクラスが実装したparseメソッドが実行されています。
まとめ
ひとまず、Pythonで動的にクラスロードすることができました。
本来であれば、BaseParserクラスを継承しているか、継承していない場合のエラーハンドリングのチェックが必要ですが、今回の実装では行っていません。
ソースコードはこちらのRepositoryに配置しておきました。