Pythonライブラリを小分けにInstallできるレポジトリ設計

# 概要

Python 名前空間パッケージによるライブラリ分割フレームワーク [Microlib](https://blog.shazam.com/python-microlibs-5be9461ad979) のアイデアにより、大きなライブラリを機能別にインストールでき、かつ単一リポジトリで開発するための手法を紹介します。


# 問題点として

ライブラリの開発規模が大きくなるに連れて、ある機能が必要とする外部ライブラリは別の機能では不要になる、という状況が起こります。
例えば画像処理やネットワーク処理および機械学習を行うライブラリを開発する場合、 OpenCV や Tensorflow などの外部ライブラリを担当外の機能にインストールするには規模が大きく効率が悪くなります。


# 実現するための手段として

[こちら](https://blog.shazam.com/python-microlibs-5be9461ad979)のブログ記事に書かれている、名前空間パッケージを使用したライブラリ分割のアイデア、  **Python microlib** により、各機能ごとの独立性を高めます。

ブログ記事中には5つのゴールが設定されていますが、この Qiita 記事では特につぎの点を取り上げます。

- 各 microlib はそれぞれが "pip install" 可能で、インストール時にはその microlib が必要な外部ライブラリをインストールする。
- microlib が他の microlib に依存する場合は、インストール時に他の microlib も自動的にインストールされる。
- 全ての microlib を含むライブラリ全体を1つのパッケージとしてインストールすることもできる。


# Python の名前空間パッケージについて

## 概要

- 名前空間パッケージは通常のパッケージで必要なマーカーファイル `__init__.py` を置く必要はありません。
- 同名の名前空間パッケージは別の場所に保存されていても自動的にまとめられます。

## 例

次のような構成となっている Python ライブラリがあるとします。
2箇所にある `mylib` には `__init__.py` はありません。 この `mylib` は名前空間パッケージになります。

```
.
|____group1
| |______init__.py
| |____mylib        ← 名前空間パッケージ
|   |____func_a.py
|____group2
  |______init__.py
  |____mylib        ← 名前空間パッケージ
    |____func_b.py

```


```Python:group1/mylib/func_a.py
def create_message1() -> None:
    return "Message 1"
```


```Python:group2/mylib/func_b.py
def create_message2() -> None:
    return "Message 2"
```

このサンプルを実行する場合

```Python
>>> from group1.mylib.func_a import create_message1
>>> from group2.mylib.func_b import create_message2
>>> print(create_message1())
Message 1
>>> print(create_message2())
Message 2
```

mylib は通常のパッケージではありませんが、名前空間パッケージとして読み込まれます。
これにより中の関数を実行することができました。


# microlib フレームワーク

## 概要

ブログ記事中で紹介されている構成は通常のパッケージと名前空間パッケージの組み合わせです。

```
.
|____microlibs
  |____bar                ← pip install 可能なトップレベルパッケージ
  | |____requirements.txt
  | |____setup.py
  | |____macrolib         ← 名前空間パッケージ
  |   |____bar            ← 通常のパッケージ
  |     |______init__.py
  |     |____module1.py
  |     |____moduleN.py
  |____foo                ← pip install 可能なトップレベルパッケージ
    |____requirements.txt
    |____setup.py
    |____macrolib         ← 名前空間パッケージ
      |____foo            ← 通常のパッケージ
        |______init__.py
        |____module1.py
        |____moduleN.py
```

- microlibs/bar と microlibs/foo はそれぞれが独立して pip install 可能な通常の Python パッケージです。
- 紛らわしいのですが `macrolib` が名前空間パッケージになります。
- これにより個別にインストールされても macrolib は共通の名前空間としてまとめられます。
- 使用する側は `from macrolib.bar.module1.py import func_a` や `from macrolib.foo.module2.py import func_b` のように呼び出すことができます。


## 例

### サンプルプログラム

Microlib フレームワークを実現する最小限のサンプルを作成しました。
[サンプルプログラム: mtools](https://github.com/smn-ailab/tshimura_qiita/tree/master/microlib_tools)

```
.
|____setup.py                 ← 全体を一括して pip install する場合のための setup スクリプト。
|____README.md
|____microlibs
  |____message_util_module    ← pip install 可能なトップレベルパッケージ
  | |____mtools               ← 名前空間パッケージ
  | | |____message_util       ← 通常のパッケージ
  | |   |______init__.py      ← 通常のパッケージに必要なマーカーファイル
  | |   |____greeting.py      ← 挨拶文を返す関数 `get_greeting_message` が入っています。
  | |____setup.py             ← mtools.message_util を pip install するための setup スクリプト。
  | |____requirements.txt     ← PyPi 以外の外部ライブラリがある場合はこちらに記述します。
  |____clock_util_module      ← pip install 可能なトップレベルパッケージ
    |____mtools               ← 名前空間パッケージ
    | |____clock_util         ← 通常のパッケージ
    |   |______init__.py      ← 通常のパッケージに必要なマーカーファイル
    |   |____local_time.py    ← 各地域の現地時刻を返す `get_local_time` が入っています。
    |____setup.py             ← mtools.clock_util を pip install するための setup スクリプト。
    |____requirements.txt     ← PyPi 以外の外部ライブラリがある場合はこちらに記述します。
```

- このサンプルは1つのリポジトリとして管理されています。
- 共通の名前空間 `mtools` が使用可能になります。
- 2つの microlib、 `message_utils_module` と `clock_util_module` を内包しています。
- この2つの microlib は個別に pip install することが可能です。
- また一括して pip install することも可能です。
- message_util_module をインストールすると自動的に clock_util_module がインストールされます。

以下、サンプルを基に解説します。


### インストール

まず、全ての microlib を一括してインストールする場合の例です。
このサンプルのルートで実行した場合です。

```Bash
$ pip install .
(略)
Installing collected packages: mtools
  Running setup.py install for mtools ... done
Successfully installed mtools-0.1.0

$ pip freeze | grep mtools
mtools==0.1.0
mtools-clock-util==0.1.0
mtools-message-util==0.1.0
```

1回の pip install で全ての microlib がインストールされました。
トップディレクトリの setup.py の中で、各 microlib を個別に pip install するコードが実行されたためです。

次に各 microlib を個別にインストールする場合の例です。

```Bash
$ cd microlibs/clock_util_module/
$ pip install -r requirements.txt
(略)
Installing collected packages: mtools-clock-util
  Running setup.py install for mtools-clock-util ... done
Successfully installed mtools-clock-util-0.1.0

$ pip freeze | grep mtools
mtools-clock-util==0.1.0

$ cd ../message_util_module/
$ pip install -r requirements.txt
(略)
Installing collected packages: mtools-message-util
  Running setup.py install for mtools-message-util ... done
Successfully installed mtools-message-util-0.1.0

$ pip freeze | grep mtools
mtools-clock-util==0.1.0
(略)#egg=mtools_message_util&subdirectory=microlib_tools/microlibs/message_util_module
```

各々のディレクトリで `pip install -r requirements.txt` を行い, それぞれの microlib をインストールすることができました。
requirements.txt の最後の行に `-e .` が入っているのが肝心な点になります。
これにより自身の microlib と依存 microlib がインストールされる仕組みになっています。

#### 依存する外部ライブラリや 他の microlib モジュールについて

ブログ記事中でも setup tools と requirements.txt の区別が難しい点が書かれています。

- setup.py
  - PyPiからインストールできるパッケージはこちらに記載しインストールします。
- requirements.txt
  - PyPi以外のパッケージをここに記述します。
  - 依存する microlib もここに記述します。

ブログ記事中では次のような工夫がされています。
- トップディレクトリから一括してインストールする場合
  - `pip install .` もしくは　`pip install -e .` によりトップディレクトリの setup.py を実行して、コードにより各 microlib をインストールします。

- microlib を個別にインストールする場合
  - `pip install -r requirements.txt` により、依存する他の microlib と共にインストールします。


### 実行

結果的にこのサンプルは次のような動作をします。

```Python
>>> from mtools.clock_util.local_time import get_local_time
>>> dt = get_local_time("Asia/Tokyo")
>>> dt.strftime('%Y/%m/%d %H:%M:%S')
'2018/07/09 17:09:51'

>>> from mtools.message_util.greeting import get_greeting_message
>>> get_greeting_message("Asia/Tokyo")
'こんにちは。'
```

複数のディレクトリに存在する `mtools` の各機能はまとめられ、あたかも1つのパッケージであるかのように実行することができました。


# 今後の課題と感じられた点

- 一括インストールした場合、各 microlib をまとめるだけで機能を持たないトップレベルのパッケージもインストールされる。
- 個別に microlib をインストールする場合は `pip install -r requirements` になるため紛らわしい。
- アンインストールする際は各 microlib を1つづつ `pip uninstall` する必要がある。



## 追記

(2018/07/27)

pip 10系を使用する場合はサンプルで使用されている `pip.main()` が使用できなくなっています。
その場合は次のコードで同じように動作します。

```Python
import subprocess
subprocess.run(["pip", "install", x], stdout=subprocess.PIPE)
# pip install -e . の場合は
subprocess.run(["pip", "install", "-e", "."], stdout=subprocess.PIPE)
```


# 参考
[Microlib](https://blog.shazam.com/python-microlibs-5be9461ad979)
[PEP-420](https://www.python.org/dev/peps/pep-0420/)
[Python 3.3b1 の名前空間パッケージを試してみた](http://www.freia.jp/taka/blog/hello-python-3.3b1/index.html)