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


概要

Python 名前空間パッケージによるライブラリ分割フレームワーク Microlib のアイデアにより、大きなライブラリを機能別にインストールでき、かつ単一リポジトリで開発するための手法を紹介します。

問題点として

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

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

こちらのブログ記事に書かれている、名前空間パッケージを使用したライブラリ分割のアイデア、 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

group1/mylib/func_a.py
def create_message1() -> None:
    return "Message 1"
group2/mylib/func_b.py
def create_message2() -> None:
    return "Message 2"

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

>>> 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_afrom macrolib.foo.module2.py import func_b のように呼び出すことができます。

サンプルプログラム

Microlib フレームワークを実現する最小限のサンプルを作成しました。
サンプルプログラム: mtools

.
|____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_moduleclock_util_module を内包しています。
  • この2つの microlib は個別に pip install することが可能です。
  • また一括して pip install することも可能です。
  • message_util_module をインストールすると自動的に clock_util_module がインストールされます。

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

インストール

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

$ 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 を個別にインストールする場合の例です。

$ 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 と共にインストールします。

実行

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

>>> 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() が使用できなくなっています。
その場合は次のコードで同じように動作します。

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

参考

Microlib
PEP-420
Python 3.3b1 の名前空間パッケージを試してみた