LoginSignup
17
23

More than 5 years have passed since last update.

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

Posted at

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

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

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

17
23
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
23