Pythonで、あるディレクトリ内に作成された関数を__init__.pyに自動で追加する

(良きタイトルが思いつかず、なんのこっちゃ感があってよくないですね。。)

Pythonで以下のようなことがしたいということがありました。

  1. あるディレクトリfoo以下にあるファイルに定義された関数をfoo/__init__.pyに追記することなくfoo.{関数名}()のように呼び出したい
  2. アンダースコア(_)から始まるファイルに記述された関数は追加しない
  3. ファイル内でimportされた関数やアンダースコア(_)で始まる関数は追加しない

その方法について、備忘録的にまとめます。

やりたいことの説明

想定として、fooを一つのモジュールと見なすベーシックなディレクトリ構成を取る状況を考えます。(以下は全て疑似コードというか、今回の説明用に用意したもので、意味があるものではないです。)

$ tree .
.
├── foo
│   ├── __init__.py
│   ├── aaa.py
│   └── bbb.py
└── main.py

1 directory, 4 files

aaa.pybbb.pymain.py には以下のような内容が書かれています。

  • aaa.py
from os.path import abspath


def hoge():
    print('hoge: ', abspath(__file__))
  • bbb.py
def fuga():
    print(f'fuga: {_piyo()}')


def _piyo():
    return 'piyo'
  • main.py
import foo

foo.hoge()
foo.fuga()

当然何もしないで、main.py を実行したところでimportエラーとなります。

$ python main.py
Traceback (most recent call last):
  File "main.py", line 3, in <module>
    foo.hoge()
AttributeError: module 'foo' has no attribute 'hoge'

通常、この状況で foo.hoge() というような呼び出しがしたい場合は、foo.__init__.py に関数をimportしておくことによって、importエラーは解決できます。

from .aaa import hoge
from .bbb import fuga

__all__ = [
  hoge,
  fuga,
]  # 注. __all__ はなくてもよい

ただ、今回やりたかったことは、fooディレクトリ以下にimportしたい関数が増えた時にこのinit.pyに追加することなく、foo.hoge() のように呼び出したいということでした。

実現した方法

以下のコードを foo/__init__.py に記述することで実現しました。

import os
import glob
import importlib
import inspect
import types


def get_all_functions():
    current_module_path = 'foo'

    filepaths = [file for file in glob.glob(os.path.join(os.path.dirname(__file__), '[a-zA-Z0-9]*.py'))]

    functions = []
    for filepath in filepaths:
        module_name = os.path.splitext(os.path.basename(filepath))[0]

        module = importlib.import_module(f'{current_module_path}.{module_name}')

        for prop_name in dir(module):
            if not isinstance(getattr(module, prop_name), types.FunctionType):
                continue
            if prop_name.startswith('_'):
                continue

            function = getattr(module, prop_name)

            if inspect.getfile(function) != filepath:
                continue

            functions.append(function)

    return functions


for function in get_all_functions():
    locals()[function.__name__] = function

説明用のコメントを付けたのが以下です。(コードは同じものです。)

import os
import glob
import importlib
import inspect
import types


def get_all_functions():
    # モジュールパス。'bar.foo.{関数名}'と呼び出す場合はここに 'bar.foo' と書く
    current_module_path = 'foo'

    # foo以下にあるファイルの絶対パスを取得し、listで持つ
    filepaths = [file for file in glob.glob(os.path.join(os.path.dirname(__file__), '[a-zA-Z0-9]*.py'))]

    functions = []
    for filepath in filepaths:
        # モジュールとしてアクセスするために foo/aaa.pyの 'aaa' の部分を取得する
        module_name = os.path.splitext(os.path.basename(filepath))[0]

        # foo.aaa をimportする
        module = importlib.import_module(f'{current_module_path}.{module_name}')

        # foo.aaa に定義されているプロパティに順にアクセスする
        # この時、定義した関数だけでなく、例えばaaa.py内にimportされているモジュールや関数、変数などが全て取得される
        for prop_name in dir(module):
            # 関数でないものは除外
            if not isinstance(getattr(module, prop_name), types.FunctionType):
                continue
            # 関数名が アンダースコア('_') で始まるものは除外
            if prop_name.startswith('_'):
                continue

            # 関数の実体を取得する
            function = getattr(module, prop_name)

            # ここでaaa.py内に定義された関数でないものを除外する
            if inspect.getfile(function) != filepath:
                continue

            functions.append(function)

    return functions

# 関数名を変数とした関数の実体を、このファイルに定義し、外部からのアクセスを可能にする
for function in get_all_functions():
    locals()[function.__name__] = function

なんかもう少しスマートな方法や考慮漏れがあるかもですが、これで実現することができました。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.