python
結論
パッケージのルートディレクトリからサブディレクトリ内のスクリプトsubdir/script.pyを実行するときはpython -m subdir.script
とするか、スクリプトの先頭に
import.sys; sys.path.insert(0, "./")
と書いておいてから
python subdir/script.py
とする。
解説
Pythonプロジェクトで、直接実行する用のスクリプトをサブディレクトリに配置したいことがある。 例えば雑多なツール類をscripts/などにまとめて置きたいときだ。 このような場合スクリプトの実行のためにいちいちcdするのは面倒だから、 パッケージのルートから直接サブディレクトリ内のスクリプトを実行できるようにしたい。$ tree . ├── main.py └── scripts └── tool.py $ python scripts/tools.py hello
単純なスクリプトであれば問題ないが、tool.pyの中で親パッケージ内の別のモジュールをインポートしたいとき、 ややこしいことが起こる。
実験用に次のようなパッケージを考える。
$ tree . ├── a.py ├── subdir1 │ └── b.py └── subdir2 └── c.py $ cat subdir1/b.py def hello(): print("I am pacage b") $ cat subdir2/c.py import subdir1.b as b b.hello()このようにスクリプトsubdir2/c.pyからパッケージ内の共通ライブラリ的なモジュール subdir1/b.py内の関数を呼び出したい、という想定だ。
ところがこれはパッケージルートからは直接実行できない。
$ python subdir2/c.py Traceback (most recent call last): File "subdir2/c.py", line 9, insubdir1が見つからないとエラーが出てしまう。import subdir1.b as b ModuleNotFoundError: No module named 'subdir1'
これはなぜかというと、Pythonではスクリプト実行時のインポートパスにはコマンド実行元の カレントディレクトリではなくスクリプトの配置場所が入ってしまうためである。 インポートパスとは
sys.path
で確認できる、Pythonがimport文でパッケージを検索する対象
のパスのリストである。
これをスクリプトを少し改造して確認しよう。
$ cat subdir1/b.py import os print("cwd:", os.getcwd()) # カレントディレクトリをプリント import sys import pprint print("path:") pprint.pprint(sys.path) # インポートパスをプリント import subdir1.b as b b.hello() $ python subdir2/c.py cwd: /mnt/c/Users/user/tmp/ path: ['/mnt/c/Users/user/tmp/subdir2', # /mnt/c/Users/user/tmp/ ではない! '/usr/lib/python38.zip', '/usr/lib/python3.8', '/usr/lib/python3.8/lib-dynload', '/usr/local/lib/python3.8/dist-packages', '/usr/lib/python3/dist-packages'] Traceback (most recent call last): File "subdir2/c.py", line 9, inのように確かにカレントディレクトリimport subdir1.b as b ModuleNotFoundError: No module named 'subdir1'
/mnt/c/Users/user/tmp
ではなく
スクリプトの置き場所/mnt/c/Users/user/tmp/subdir2
がインポートパスに
指定されてしまっている。これではsubdir1にアクセスできないではないか。ちなみに相対インポートで親ディレクトリからのインポートを試みてもだめである。
$ cat subdir2/c.py from .. import subdir1.b as b b.hello() $ python subdir2/c.py Traceback (most recent call last): File "subdir2/c.py", line 12, inこれはPythonの仕様上、実行中のpyファイルはパッケージと見なされないことに由来する。 パッケージと見なされなければ親パッケージも定義されず、相対インポートは失敗するのだ。 このときに"ImportError: attempted relative import with no known parent package"という ややわかりにくいエラーメッセージとなる。from . import subdir1 ImportError: attempted relative import with no known parent package
解決策としては、1. -mオプションによるモジュール実行を行う、2. sys.pathに./を追加してsubdir1 をインポートできるようにする、の2通りがある。
解決策1. -mオプションによるモジュール実行を行う例:
$ cat subdir2/c.py # 元に戻した import subdir1.b as b b.hello() $ python -m subdir2.c I am pacage bPythonの実行時に
-m [モジュール名]
を渡すと[モジュール名]を実行するが、
このときはインポートパスが実行元のディレクトリになっている。
そのためsubdir1を問題なく参照できる。
解決策2. sys.pathに./を追加する例:
$ cat subdir2/c.py # 変更を加えた import sys sys.path.insert(0, "./") import subdir1.b as b b.hello() $ python subdir2/c.py I am pacage bsys.pathは手で編集すればそれ以降のimportで追加されたパスも見に行ってくれる。
1. と大して変わらないが、しいて言うとモジュール名
subdir2.c
にシェルのtab補完
が効かないのでファイル名などが長いときはこちらの方が楽か。