pythonでは、デフォルトでは実行フォルダおよびその配下のフォルダだけがモジュール検索対象のパスとなっています。
そのため、ある程度プロジェクトが大きくなってきて、共通モジュールを特定のフォルダに整理してまとめたりすると、パスが通らず困ったりします。
今回は、兄弟階層の共通モジュールのimport方法を説明します(もちろん親階層でも同じ方法で対応できます)。

はじめに

フォルダ構成

今回は以下のようなフォルダ構成でmain.pylogger.pyをimportする方法について解説します。

┣ module
┃ ┗ logger.py
┗ business
  ┗ main.py

logger.pyの実装

import logging

formatter = '%(asctime)s [%(levelname)s] %(message)s'
logging.basicConfig(level=logging.DEBUG, format=formatter)

def info(message: str, *args, **kwargs):
    logging.info(message, *args, **kwargs)

main.pyの実装

from module import logger

def main():
    logger.info('module imported successfully.')

if __name__ == "__main__":
    main()

問題点

moduleフォルダがモジュール検索パスに入ってないので、ModuleNotFoundErrorが発生します

$ python main.py
Traceback (most recent call last):
  File "main.py", line 4, in <module>
    from module import logger
ModuleNotFoundError: No module named 'module'

①シンボリックリンクを利用

以下のように実行ファイルが配置されているbusinessフォルダに、moduleフォルダへのシンボリックリンクを作成します。

$ cd business
$ ln -s ../module module
$ ls -la 
-rw-r--r--  1 hoge  staff  147 11 29 09:44 main.py
lrwxr-xr-x  1 hoge  staff    9 11 29 10:00 module -> ../module

この状態だと、問題なく実行できます。

$ python main.py
2019-11-29 10:02:19,624 [INFO] module imported successfully..

この方法の特徴は

  • ○ gitにcommitできて、linux環境にgit pullして問題なく動作する
  • × 実行フォルダ毎にシンボリックリンクを作成する必要がある
  • × おそらくwindowsで動かない

のような感じで、正直微妙な感じですね。

②環境変数 PYTHONPATH を利用

環境変数PYTHONPATHにモジュール検索パスを追加する方法があります。
指定するパスは、実行フォルダからの相対パス、もしくは、絶対パスとなります。
今回はfrom module import loggerという形でimportしているので、moduleフォルダの親階層python_sampleをパスに追加します(以後の対応案は全て同様です)。

# 相対パス
export PYTHONPATH="..:$PYTHONPATH"`
# 絶対パス
export PYTHONPATH="/hoge/python_sample/:$PYTHONPATH"

これも問題なく実行できました。

$ python main.py
2019-11-29 10:26:37,813 [INFO] module imported successfully.

この方法の特徴は

のような感じで、環境変数管理が面倒ですが、最有力候補ではないでしょうか。

③パス設定ファイル name.pth にパスを設定

詳細はこちらを参照いただきたいですが、lib/pythonX.Y/site-packages配下にパス設定ファイルname.pthを配置して、そこにモジュール検索パスを記載する方法があります。
site-packagesフォルダは、pythonのインストールフォルダの配下にありますので、そこに今回はpython_sample.pthファイルを作成しました。

cd /fuga/lib/python3.7/site-packages
echo "/hoge/python_sample" > python_sample.pth

このファイルを作成した状態であれば、問題なく実行できました。

$ python main.py
2019-11-29 10:57:09,094 [INFO] module imported successfully.

この方法の特徴は

  • ○ パス設定ファイルだけ管理すればよい
  • × パス設定ファイルがプロジェクト外なので、git管理もできず、ファイルの管理が難しい

のような感じだで、パス設定ファイルの管理が難しく、あまりおすすめできないです。

④実行時にモジュール検索パスリスト sys.path に追加

sys.pathはモジュール検索パスの文字列の単純なリスト(List[str])です。
実行時に、sys.pathに検索対象の文字列を追加します。main.pyに以下のような感じで実装します。

import sys
import os
sys.path.append(os.pardir) # 親ディレクトリの取得はPathを使う手もある

from module import logger

def main():
    logger.info('module imported successfully.')

if __name__ == "__main__":
    main()

from module import loggerより前にsys.pathに対象パスを追加する必要があるので、地味に面倒です。autopep8を入れていると、importがトップレベルに自動で移動され

from module import logger
import sys
import os
sys.path.append(os.pardir)

のようになり、from module import loggerする時点でsys.pathに対象パスが追加されておらずうまく動きません。

なので、autopep8の該当動作をOFFにする必要があります。VSコードの場合だと、.vscode/settings.jsonに以下の設定を追加します。

"python.formatting.autopep8Args": [
    "--ignore=E402",
],

さらに、flake8を入れている場合、そちらでmodule level import not at top of fileflake8(E402)というワーニングが出るので、そちらのチェックもOFFにする必要があります。.vscode/settings.jsonに以下の設定を追加します。

"python.linting.flake8Args": [
    "--ignore=E402",
],

この方法の特徴は

  • ○ 全てソースコード上で管理できる。当然git管理もできる
  • × 全部の実行ファイルに実装が必要
  • × 遠い場所にあるフォルダだったりすると、パスを追加する実装がより煩雑
  • × pep8のE402に準拠できなくなる

のような感じで個人的には、ソースコード上で全てが分かるのはポイント高いので、これもありだと思っています。

⑤ module 配下をパッケージにしてpipインストール

プロジェクト直下に、setup.pysetup.cfgの二つのファイルを準備して、パッケージを作成します。
まず、以下のようにsetup.pyを作成します。

from setuptools import setup
setup()

次に、以下のようにsetup.cfgを作成します。moduleフォルダ配下をmoduleというパッケージにしています。

[metadata]
name=module
version=0.1.0

[options]
packages = module

その上で、pipコマンドを実行するとパッケージが作成され、site-packagesにインストールされます。

$ pip install -U .
Processing /hoge/python_sample
Installing collected packages: module
    Running setup.py install for module ... done
Successfully installed module-0.1.0

この状態であれば、問題なく実行できました。

$ python main.py 
2019-11-29 12:47:15,190 [INFO] module imported successfully.

この方法の特徴は

  • ○ 全てソースコード上で管理できる。当然git管理もできる
  • × 共通モジュールに変更が入った際、pip install し直す必要がある
    • 開発時、他の人が共通モジュールに手を入れて、git pullしたけど、pip installし忘れて古いバージョンで実装、みたいなことが発生する可能性大

のような感じで、とても正統派な感じなのですが、古いバージョンで実装してしまうのが怖いので、個人的にはやめておこうと思いました。

さいごに

どれも一長一短ですが、個人的には環境変数の管理をプロジェクト内でちゃんとやる前提で
②環境変数PYTHONPATHを利用
が一番しっくりくると感じました。