SANDFISH FACTORY

技術ブログです。python・vuejsを愛でる日々について綴ります

Pythonで動的にクラスロードして実行してみた

今、個人的な開発でオープンデータを取集して解析するコード書いています。

オープンデータで公開しているファイルの書式がバラバラでparserを多く書くことになりそうなんで、pythonで動的なクラスロードの仕組みを調べました。
(parserを追加するごとに呼び出しもとをコードを修正するのは大変なので)

実現したいこと

  • parserは所定のディレクトリに配置されたら、動的に読み込まれる対象となる
  • 読み込まれるモジュールはparserの基底クラスを継承している
  • 基底クラスの特定メソッドを呼び出して処理を実行する

Pythonで動的にクラスロードして実行

前提条件

以下の環境で試しました。

抽象クラスとabstract method

検索してみると抽象基底クラス(Abstract Base Class:ABC)というものが準備されているとのことです。

abc --- 抽象基底クラス — Python 3.7.6 ドキュメント

以下の構成でファイルを作りました。

f:id:sandfish03:20200224150134p:plain

  • main.py:実行モジュールです
  • parserディレクト
    • base_parser.py:抽象基底クラスです
    • bunkyoku_parser.py:baser_parserクラスを継承した文京区用パーサークラスです
    • komaeshi_parser.py:baser_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")

f:id:sandfish03:20200224152143p:plain

echoメソッドではなく、parseメソッドに変えてからBunkyokuParserクラスをインスタンス化するとエラーにならず、正常に実行できました。

# bunkyoku_parser.py
from parser.base_parser import BaseParser

class BunkyokuParser(BaseParser):
    def parse(self):
        print("echo bunkyoku")

f:id:sandfish03:20200224153031p:plain:w400

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

実行した結果はこちらです。 f:id:sandfish03:20200224181812p:plain

各モジュールに含まれるBaseParserクラスを継承したクラスが実装したparseメソッドが実行されています。

まとめ

ひとまず、Pythonで動的にクラスロードすることができました。
本来であれば、BaseParserクラスを継承しているか、継承していない場合のエラーハンドリングのチェックが必要ですが、今回の実装では行っていません。

ソースコードはこちらのRepositoryに配置しておきました。

github.com