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.py`、`bbb.py`、`main.py` には以下のような内容が書かれています。

- `aaa.py`

```python
from os.path import abspath


def hoge():
    print('hoge: ', abspath(__file__))
```

- `bbb.py`

```python
def fuga():
    print(f'fuga: {_piyo()}')


def _piyo():
    return 'piyo'
```

- `main.py`

```python
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エラーは解決できます。

```python
from .aaa import hoge
from .bbb import fuga

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

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


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

```python
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
```

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

```python
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
```


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