環境
Python 3.9
結論
以下のいずれかの方法をとれば基本的にモジュールのimportでトラブルことはない
- プロジェクトルートをPYTHONPATHに追加する
- 対象のスクリプトやライブラリをモジュールとして実行する。例えばテストの実行であれば、python -m pytestの形式で実行してあげる
モジュールについて
パスはどこに定義されている?
ここはすでに知識として知っていれば、読み飛ばしても問題ないかと思われる。
解決可能なモジュールのパスはsys.pathに定義されている。sys.pathの中身は以下のように確認できる
import sys
print(sys.path)
標準ライブラリと外部パッケージ
sysやdatetimeなどの標準ライブラリやpandasなどの外部パッケージでモジュールimportの問題に引っ掛かることは基本的にはない。なぜならそれらのモジュールに対してのパスはsys.pathにすでに定義されているから。
python -c "import sys; print('\n'.join(sys.path))"
/Users/usename/.pyenv/versions/3.9.18/lib/python3.9
・・・・
利用者は特に意識することなくモジュールをimportすることができる。
実行ファイルが配置されているディレクトリ内のモジュール
実は実行ファイルが配置されているディレクトリも、そのファイルが実行されるときにパスに追加されている。例えば、'/Users/usename/git/project-root/shared/tests/test_hoge.py'を実行すると、テスト実行時にこのテストファイルが配置されているディレクトリである'/Users/usename/git/project-root/shared'がパスに追加されることになる
その他細かくそれ以外もあるが、特に意識しなくてもimportできるモジュールの種類は、基本的には上記を理解しておけば間違いない。以降では、上記に該当しないような、例えば自作モジュールで実行ファイルが配置されているディレクトリとは別のディレクトリにあるモジュールのimpotに失敗するケースや失敗時に発生するエラーなどについて確認する
モジュールのimportに失敗したら出るエラー
対象のモジュールの実体がプロジェクトに存在していたとしても、プログラムを実行する際にそのパスが解決できていなければモジュールが見つからないと言われる
ModuleNotFoundError: No module named 'module_a'
ディレクトリ例
以下のようなディレクトリ構成だとする
mymodule
┗module_a
┗script_a.py
┗module_b
┗script_b.py
main.py
このような構成の場合、ルートディレクトリにあるファイル(main.py)からはどのモジュールも問題なくimport可能。なぜならmain.py実行時にmain.pyが配置されているディレクトリであるmymoduleディレクトリ自体のパスがsys.pathに追加されることにより、mymodule以下のモジュールが全て探索可能なモジュールとして設定されるからである。
main.py
# これは問題なくimportできる
from module_a.script_a import func_a
from module_b.script_b import func_b
問題となるケース
一方、module_a/script_a.pyからmodule_b/script_b.pyないしその逆いずれもimportできない。なぜなら各ファイルを実行する際にsys.pathには、mymoduleではなく、script_a.pyであれば、module_aが、script_b.pyであればmodule_bがsys.pathに追加されるためである。
つまり、module_a/script_a.pyであれば、以下のようにmodule_a以下のモジュールを、module_b/script_b.pyであれば、module_b以下のモジュールのみがimport可能なモジュールとなる
module_a/script_a.py
# これは不可
import module_b.script_b
# これは可(module_aディレクトリ内にあるscript_c.py)
from script_c import func_c
ただし、from script_c import func_c というimport方法は、実はmain.pyから実行する場合は正常に実行できない。
すでに説明した通り、main.pyから実行する場合はmymodule以下のモジュールが全て探索可能なモジュールとして設定されるが、script_cというモジュールの指定方法だと特定できないため、正確にはfrom module_a.script_c import func_cとする必要がある。
具体的にどういったケースで不都合が生じるか
プロダクトコードであればそこまで問題になることはないが、特にモジュールのimportはテストを作成する際に問題となる。例えばプロジェクトルートにtestsディレクトリを作成し、tests/test_script_a.pyを作成したとする
from module_a.script_a import func_a
def test_サンプル():
assert func_a() == 4
$ pytest
このとき、module_aディレクトリにあるscript_aをimportしようとしているが、これは失敗する。testsディレクトリ以下のファイルが実行の起点となるので、テスト対象スクリプトが存在するのmodule_aやmodule_b内のファイルを解決できない。
そのため、一番手っ取り早いのは、実行するファイルを起点として動的にsys.pathに追加するのではなく、PYTHONPATHにmymoduleまでのパスを指定しておき、importしたい場合は、起点として実行するファイルに関係なく以下のようにmodule_a、module_bからimportするようにすればいい。
このようにしておけば、main.pyを実行しようが、testディレクトリのテストを実行しようが、moduleディレクトリ内のスクリプトを実行しようが、モジュールのパスが問題なく解決できる
from module_a.script_a import func_a
from module_b.script_b import func_b
もしくは、プロジェクトのルートにpytest.iniを作成し、以下のようにpythonpathにテスト対象のモジュールが配置されているディレクトリのパスを指定してあげれば、OK
[pytest]
addopts = --cov-report html --cov=src
pythonpath = "src"
testpaths = "tests"
PYTHONPATH
環境変数PYTHONPATHとして指定できる。Linux系OSの場合は:(コロン)で、Windowsの場合は;(セミコロン)で区切ることで複数のパスを解決先の候補に加えることが可能
export PYTHONPATH=/hoge/foo/bar
設定されているPYTHONPATHを確認する方法
export -p
または
printenv
もしくは、pytestをモジュールとして指定して実行する方法であれば、実行時にカレントディレクトリをsys.pathに追加してくれるので、問題なくプロダクトコードのモジュールのパスを解決できる
$ python -m pytest
なお、以下のようにモジュールのパスを追加できるが、これはPythonのスタイルガイド(コーディング規約)であるPEP8では非推奨とされている
sys.path.append('設定したいパス')
まとめ
いかがでしたでしょうか。本記事では、pythonのモジュールのimportの仕組みや仕様、最低限押さえておきたいポイントについて紹介しています。この辺りを理解しておくことは効率的かつ無駄のないディレクトリ設計、モジュール作成にとってとても重要な内容となりますので、ぜひ参考にしてみてください。