(良きタイトルが思いつかず、なんのこっちゃ感があってよくないですね。。)
Pythonで以下のようなことがしたいということがありました。
- あるディレクトリ
foo
以下にあるファイルに定義された関数をfoo/__init__.py
に追記することなくfoo.{関数名}()
のように呼び出したい - アンダースコア(
_
)から始まるファイルに記述された関数は追加しない - ファイル内でimportされた関数やアンダースコア(
_
)で始まる関数は追加しない
その方法について、備忘録的にまとめます。
やりたいことの説明
想定として、fooを一つのモジュールと見なすベーシックなディレクトリ構成を取る状況を考えます。(以下は全て疑似コードというか、今回の説明用に用意したもので、意味があるものではないです。)
$ tree .
.
├── foo
│ ├── __init__.py
│ ├── aaa.py
│ └── bbb.py
└── main.py
1 directory, 4 files
aaa.py
、bbb.py
、main.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
なんかもう少しスマートな方法や考慮漏れがあるかもですが、これで実現することができました。