Pythonのモジュール読み込みの挙動(シェル直接とIpython上での違い)

シェルから直接pythonスクリプトを実行した時と、Ipython上でスクリプトを実行した際に、モジュールの読み込み挙動が異なりしばらくハマったため、その違いをまとめ共有します。

結論

思っていたよりも長い記事となってしまったため、先に結論を書いておきます。

pythonのデフォルトでは 実行するスクリプトのあるフォルダ でのモジュールの探索は行われるが、シェルのカレントディレクトリでの探索は行わない。

Ipythonでは Ipythonの機能として Ipythonのカレントディレクトリでのモジュール探索も行われる。

前提

普段、私は以下のように libs/ フォルダ以下に自作のモジュールやパッケージを置き、 scripts/ フォルダ以下にスクリプトを置いています。

project_root
|-- libs
|   |-- my_module_01
|   `-- my_module_02
`-- scripts
    |-- script_01
    `-- script_02

そして、 project_root/ でIpythonを起動し解析を行っています。スクリプトの実行もIpythonの中で run scripts/script_01 のようにして実行しています。

ハマった所

Ipythonでは問題なく実行できるスクリプトを、シェルから直接pythonを実行するとエラーが出ました。 以下のような簡単なモジュールとスクリプトを例として説明します。

# prpject_root/libs/my_module.py
def hello():
    print('hello!')


# project_root/scripts/call_module.py
from libs.my_module import hello

if __name__ == '__main__':
    hello()

スクリプト call_module.py は、 libs/ フォルダ下にある my_module.py から hello という関数を読み込んでいます。 このスクリプトproject_root で起動したIpython上では問題なく実行できますが、同じく project_root をカレントディレクトリとしたシェル上ではエラーが出ます。

# Ipython上での実行
pwd
>>> .../project_root  # カレントディレクトリの確認

run scripts/call_module.py
>>> hello!


# bash上での実行
pwd
>>> .../project_root  # カレントディレクトリの確認

python scripts/call_module.py
>>> Traceback (most recent call last):
>>>   File "scripts/call_module.py", line 1, in 
>>>     from libs.my_module import hello
>>> ModuleNotFoundError: No module named 'libs'

上記のように、シェル上で call_module.py を実行しようとすると、 libs が見つからないというエラーが出ます。

問題点

libs が見つからないということは、シェル上で起動すると project_root/libs/ へのpathが通っていないということに成ります。しかし、この時私はPythonはカレントディレクトリもモジュールの探索に使用するはずと思っていたためになぜエラーが出たのかが分かりませんでした。

そこで、次のカレントディレクトリとpathを確認するためのスクリプトscripts/ フォルダ下に置き、確認を行いました。

# project_root/scripts/check_path.py
import os
import sys

if __name__ == '__main__':
    print('pwd:', os.getcwd(), '\n')
    
    for place in sys.path:
        print('path:', place)

このスクリプトを実行すると以下の結果が得られます。

# Ipython上での実行
run scripts/check_path.py
>>> pwd: .../project_root
>>>
>>> path:
>>> path: .../project_root/scripts
>>> # 以下、個別に設定したパス


# bash上での実行
python scripts/check_path.py
>>> pwd: .../project_root
>>>
>>> path: .../scripts
>>> # 以下、個別に設定したパス

Ipython上で実行した際に空白のpathがありますが、これはIpythonのカレントディレクトリ、すなわち .../project_root を意味します。一方、シェル上で実行した際にはカレントディレクトリにpathは通っていません。 また、両者ともに実行したスクリプトの置いてある project_root/scripts へのpathは通っています。

つまり、pythonのデフォルトでは 実行するスクリプトのあるフォルダ でのモジュールの探索は行いますが、シェルのカレントディレクトリでの探索は行わないようです。 そして、Ipythonでは Ipythonの機能として Ipythonのカレントディレクトリでのモジュール探索が行われるようです。

私は、このIpythonでの機能をpythonのデフォルトだと勘違いしていたためハマることと成りました。

以上より、Ipythonおよびシェルではモジュールの読み込みの挙動が異なるため、自作のモジュールを使用するスクリプトをIpython・シェルの両方で実行する可能性がある場合は、素直にモジュールのあるフォルダへのpathを事前に登録すべきのようです。