pythonで自作モジュールをimportする際の対処方法がよくわからなくなってしまったのでメモ
背景
pythonに限らずプラグラミングをしていると、最初1ファイルで記載していた処理を複数ファイルに分割したくなってきます。
そしてそのうち分割したファイルをディレクトリに分けて管理したくなります。
これらのディレクトリに分けて管理を始めたファイルをimportする際に少し工夫が必要で手間取ったので対応方法を残しておきます。
なお、細かいですが、パターンが以下の2つに別れます。この記事では1の方について記載します。
1.実行ファイル内で親ディレクトリ経由のパスにあるモジュールをimportしたい場合
.
├package1
│ ├__init__.py
│ └module1.py # <= 実行ファイル
└package2
├__init__.py
└module2.py
↑実行ファイルがmodule1.pyで、module1.pyからmodule2.pyを呼び出したい場合
2.実行ファイルがimportしているモジュールの中で、親ディレクトリ経由のパスにあるモジュールをimportしたい場合
.
├package1
│ ├__init__.py
│ └module1.py
├package2
│ ├__init__.py
│ └module2.py
└module3.py # <= 実行ファイル
↑実行ファイルがmodule3.pyで、module3.pyがmodule1.pyをimport、module1.pyがmodule2.pyをimportしたい場合
もう少し具体的に
共通処理の切り出しまで
pythonで何か機能を開発するとき、最初は1ファイルだったのが、2ファイル、3ファイルと増えていくに連れ、共通処理を切り出して、モジュール化したくなると思います。
初期状態
python_import_test
├functionA.py
├functionB.py
└functionC.py
↓
共通処理をcommonパッケージに切り出し
python_import_test
├common
│ ├util.py
│ └__init__.py # <= python3.3以降は__init__.pyなしでもimport可能
├functionA.py
├functionB.py
└functionC.py
common/util.pyの中身
import os
def test():
print("this method is in %s" % os.path.basename(__file__))
functionA.pyの中身
from common import util
print("this is %s" % __file__)
util.test()
実行
$ pwd
/python_import_test
$ python functionA.py
this is functionA.py
this method is in util.py
本題:ディレクトリを分けて管理をしたい
ここまでは特に問題ないと思うのですが、この後さらにファイルが増え、以下のようにfunctionごとにディレクトリを分けて管理したくなってきます。
ディレクトリ管理前
.
├common
│ ├db.py
│ └__init__.py
├functionA_1.py
├functionA_2.py
├functionA_3.py
├functionB_1.py
├functionB_2.py
└functionB_3.py
↓
ディレクトリ管理後
.
├common
│ ├util.py
│ └__init__.py
├functionA
│ ├functionA_1.py
│ ├functionA_2.py
│ └functionA_3.py
└functionB
├functionB_1.py
├functionB_2.py
└functionB_3.py
試しに以下のように記載して実行しようとしてみたところ、エラーが発生します。
from ..common import util
print("this is %s" % __file__)
util.test()
$ pwd
/python_import_test/functionA
$ python functionA_1.py
Traceback (most recent call last):
File "functionA_1.py", line 1, in <module>
from ..common import util
ImportError: attempted relative import with no known parent package
pythonは実行時にsys, built-in, mainの3つを初期化します。
mainの初期化の際に実行ファイルの上位ディレクトリ(上の例で言うfunctionAディレクトリ)はモジュールの探索パスに含まれないので、いざimportしようとしても探索にひっかからずエラーとなってしまいます。
解決策
自分で調べた限りだと以下2つの解決策がありました
- モジュール探索パスを追加する
- 親ディレクトリを環境変数PYTHONPATHに通す
1. モジュール探索パスを追加
pythonではimportはモジュールの探索 => ロードの順に実行されます
モジュールの探索対象のパスがsys.pathに格納されているので、そこに親ディレクトリのパスを追加してあげると正常にimportが実行されます
import os
import sys
sys.path.append(os.pardir)
from common import util
print("this is %s" % __file__)
util.test()
$ pwd
/python_import_test/functionA
$ python functionA_1.py
this is functionA_1.py
this method is in util.py
ただし、これはPythonのコーディング規約であるPEP8に違反しており(規約ではfromとimportをファイルの一番上に記載することになっているが、sys.path.appendを先に記載しないとimportができないため)、文法チェックツールを使っていると怒られます(動作自体はするのであまり問題になりませんが)
2. 親ディレクトリを環境変数PYTHONPATHに通す
sys.pathをいじるのが邪道だと感じる場合はこちらの方法があります(おそらくこっちが王道?)
ローカル実行のみであれば、.bashrcに記載しておけば良いのですが、Dockerコンテナ内で.pyファイルを動かしたい場合は、ENVコマンド内で設定してあげる必要があります。
.bashrc
export PYTHONPATH="<python_import_testまでのフルパス>:$PYTHONPATH"
Dockerfile
ENV PYTHONPATH "<python_import_testまでのフルパス>:$PYTHONPATH"
from common import util
print("this is %s" % __file__)
util.test()
環境変数の設定は手間なので固く作るときは2を、それ以外は1を使う感じかなと思いました。