Pythonのimportについて調べる。
想定
プロジェクトのルートディレクトリから、子孫のディレクトリにあるスクリプトの実行を想定する。 そのスクリプトでは、親兄弟のディレクトリのモジュールをインポートする。
参考
公式docsを見るのがいい。importで検索すれば、すぐに見つかる。
6. モジュール — Python 3.8.0 ドキュメント
6.1.2. モジュール検索パス spam という名前のモジュールをインポートするとき、インタープリターはまずその名前のビルトインモジュールを探します。見つからなかった場合は、 spam.py という名前のファイルを sys.path にあるディレクトリのリストから探します。 sys.path は以下の場所に初期化されます:
- 入力されたスクリプトのあるディレクトリ (あるいはファイルが指定されなかったときはカレントディレクトリ)。
- PYTHONPATH (ディレクトリ名のリスト。シェル変数の PATH と同じ構文)。
- インストールごとのデフォルト。
説明
引用1つ目はよく分かる。シェルで実行した時のファイルのあるディレクトリだろう。
プロジェクトのルートディレクトリから子や孫フォルダの中のスクリプト(Aとする)を実行すると、この1つ目のために、Aのディレクトリの親ディレクトリや兄弟ディレクトリのモジュールをインポートできない。
# root/dirA/scriptdir/script.py
print('dirA.scriptdir.script')
# このままでは失敗する。 ModuleNotFoundError: No module named 'dirA'
import dirA.a
import dirA.dirB.b
cd root_dir && python dirA/scriptdir/script.pyは、ModuleNotFoundError: No module named 'dirA'によって失敗してしまう。
方法1(sys.pathの利用)
sys.pathが上の引用のように初期化されているので、失敗する。なので、初期化の後に修正すればいい。
これは、よく見る方法だと思う。
# 先ほどと同じファイル。
# sys.pathを修正する。プロジェクトのルートディレクトリをsys.pathに追加する。
print('dirA.scriptdir.script')
import sys
import pathlib
scriptdir = pathlib.Path(__file__).parent
dirApath = scriptdir.parent
rootdir = dirApath.parent
# project root dir path will be appended
sys.path.append(rootdir.as_posix())
import dirA.a
import dirA.dirB.b
# ここまで到達できる。
pathlibモジュールを使うと楽だと思う。
cd root_dir && python dirA/scriptdir/script.pyはエラーせずに終了できる。
方法2(PYTHONPATHの利用)
引用の2つ目を見ると、PYTHONPATHという環境変数を利用できることがわかる。dockerのpythonイメージでも使われていたと思う。
ルートディレクトリで実行するので、ルートディレクトリを登録すればいい。
なので、最初と同じようなスクリプトで、
# root/dirA/scriptdir/script2.py
print('dirA.scriptdir.script2')
import dirA.a
import dirA.dirB.b
cd root_dir && PYTHONPATH=. python dirA/scriptdir/script2.pyでエラーせず終了する。
また、export PYTHONPATH=.でも良い。dotenvなどもあるだろうと思う。
PYTHONPATHは、PATHと同じようにコロン(:)区切りなど(環境による。os.pathsepのもの)で複数登録可能。
あとがき
個人的には方法2が好み。環境などによると思うが、方法はいろいろあるということを知っておいたほうがいいだろう。

