ChangeLog
- 2020-04-18:
__main__
モジュールにおける振る舞いについて訂正。
概要
なんとなくわかった気になって使っている人が多そうな、Python のパッケージとモジュールについてざっくりと勉強します。
あまりまとまってないのでいつか書き直す。
動機
他人の書いた Python の機械学習のコードがあります。
カレントディレクトリからみて ./a/b/c.py
の場所に実行したいファイルがあります。
このファイルは __name__
の値によって2通りに動作する、よくある形式で書かれています。
中では ./a/b/d.py
を import d
によって 絶対 import しています。
このとき、PYTHONPATH
をデフォルトのままと仮定すると、python ./a/b/c.py
とした場合と、他の python ファイルから import a.b.c
とした場合で、どちらが正しく動作するかが変わります。
前者が動いている状態では後者が動かず、後者が動くように、c.py
内を相対 import に書き換えると前者が動かなくなります。
一体何が起こっているのだろうかというのが、今回勉強する動機です。
後者の状況をもう少し説明します。ユニットテストのために pytest ./test/<テストファイル>.py
もしくは python -m pytest ./test/<テストファイル>.py
を実行しています。テストファイルがカレントディレクトリにないことがポイントです。
参考文献
いずれも v3.8.2 版を閲覧した。
- Python チュートリアル, 6. モジュール: https://docs.python.org/ja/3/tutorial/modules.html
- Python 言語リファレンス, 1. コマンドラインと環境: https://docs.python.org/ja/3/using/cmdline.html
- Python 言語リファレンス, 5. インポートシステム: https://docs.python.org/ja/3/reference/import.html
モジュール = ファイル
チュートリアルでは、トップレベルのファイル、つまり python <ファイル>
で起動されるファイルのことを スクリプト と読んでいることに注意する。
インポートする側
いちばん単純には
- import 文
import m
はモジュールm
をインポートしている - モジュール
m
とはm.py
という Python ファイル である - これにより、
m.py
内で定義されている 名前 (変数名、関数名、クラス名、など)n
にm.n
でアクセスできる
と理解する。
- 実際にはモジュール名に加えて、パッケージパスを含める必要がある場合が多い (後述)
- 実際にはパッケージも import できる (後述)
インポートされる側
- 自分自身のモジュール名は
__name__
で参照できる。上記の方法でインポートされたm
の中では、__name__
は"m"
になる - ただし、自分がトップレベルであるとき、つまり自モジュールが
python
コマンドの引数で指定されたファイルの場合、__name__
は"__main__"
になる。トップレベル環境は__main__
モジュールとして動作している、と考えるとよい - モジュールは他のモジュールをインポートできる
モジュール探索パス (sys.path
)
上記の例で、import 対象のモジュール m
、すなわち、ファイル m.py
は sys.path
(sys
モジュールの path
変数の値) に設定されているディレクトリのリストの先頭から順に検索される。デフォルトでは以下の順に並んでいる:
-
スクリプト のあるディレクトリ、指定されない場合 (
python
により対話セッションが実行された場合など) はカレントディレクトリ -
PYTHONPATH
環境変数の値 - インストールごとのデフォルト (pip でライブラリがインストールされるディレクトリなどのこと、と思われる)
ここにおいて、1つ目が曲者で、カレントディレクトリではなくて、実行されたファイルのあるディレクトリが探索される。
カレントディレクトリにあるファイルを実行した場合にのみ、両者は同じになる。
「動機」にあげた例の解決 (1)
これで「動機」に書いた問題の理由はだいたい判明した。前者の方法で、python ./a/b/c.py
を実行すると、1つ目のルールにより、sys.path
の先頭に ./a/b
が入る。そうすると import d
はまずこのディレクトリに対して検索され、実際に ./a/b/d.py
があることから import は成功する。
一方、pytest を実行した場合は、sys.path
の先頭が ./test
になるので、そもそも import a.b.c
が通らないし、そこを PYTHONPATH
の設定などで回避したとしても c.py
内の import d
が今度は通らない。
なお、 import d
を from . import d
(だっけ?) と 相対 import にすると、確か前者パターンが通らなくなる [要確認]。
sys.path
アンチパターン
なお、モジュールのディレクトリ構成が複雑な場合や、動的 import を実現しようとして、
- Python プログラム内で動的に
sys.path
を書き換えてなんとかしようとする
のは 悪手である とされている [要出典]。importlib
を駆使するのが正しいらしい。
パッケージ = ディレクトリ
前述したモジュールをディレクトリにまとめたものが パッケージ である。ディレクトリ名がパッケージ名になる。
ディレクトリは階層構造を持つことができて、これがパッケージパス (パッケージ名のドット区切りの並び) に対応する。
インポートする側
-
import a.b.c
はパッケージa.b
にあるモジュールc
を import する。すなわち、モジュールの検索対象ディレクトリにあるa/b/c.py
を import する - ディレクトリ
p
がパッケージp
として認識されるためには、p/__init__.py
ファイル を含む必要がある
ここからが、なんとなくしか理解してない人が多そう。おれもそう。
-
パッケージ自身を import する ことができる。つまり、
import a.b
とできる - パッケージ自身、たとえば
p
がインポートされたときには、p/__init__.py
をモジュールであるように実行した結果がp
の名前空間に入る - パッケージ
a.b
を import するときにはまずa
が import され、次にa.b
がインポートされる。つまり、a/__init__.py
が実行され、次にa/b/__init__.py
が実行される。 - モジュール
a.b.c
を import するときには上と同様にファイルが実行されたあと、a/b/c.py
が実行される。
インポートされる側
パッケージ p
がインポートされるとき、p/__init__.py
の中では変数 __path__
で p
のディレクトリを表す文字列が参照できる。
from ... import ...
文
- 単純な import 文で
import a.b.c
を実行した場合、モジュールc
の名前n
を参照するにはa.b.c.n
とする必要がある - ところが、
from a.b import c
とすることにより、c.n
で参照できる -
from import
できるのは- 名前
n
。from a.b.c import n
とすると、以後はn
だけで参照できる - モジュール
c
。 先ほど説明したとおり - パッケージ
b
。from a import b
で。以後はパッケージb
をa.b
ではなくb
だけで参照できる
- 名前
なお、上のようにパッケージ b
を import する場合、b/__init__.py
が実行される。このファイルのデフォルトでは空で、これが空のときはインタプリタは何もしないため、b
を import したからといって a.b.c
にアクセスできるようになったりはしない。
インポートされるモジュールからの相対インポート
他モジュールを import する際には、通常は前述の絶対 import を用いる。モジュールの検索は sys.path
に対して行われる。
上記の from import
文を使うと 相対インポート ができる。
インポートされているモジュール (つまり .py
ファイル) 内で
from . import m
from .. import m
from ..p import q
などとしてパッケージ、モジュール、名前などを相対 import できる。このときの .
や ..
は、現在のモジュールが存在する パッケージパス をベースに解決される。
つまり、現在のモジュールが a.b.m
、つまり所属パッケージが a.b
なら .
は a.b
で ..
は a
である。
なお、__main__
モジュールはパッケージパスを持たない。ここから得られる重要な帰結として、__main__
として動いているモジュールからは、相対インポートができない。
なお、
import ..p.q.m
などと出来てもいい気がするが、出来ない模様
「動機」にあげた例の解決 (2)
これで2つ目の疑問も解決。a/b/c.py
の中を from . import d
のように相対インポートに書き換えると、c
が import a.b.c
で import された場合は正しく動作するが、python a/b/c.py
として実行すると、c.py
はモジュール __main__
として動作するため相対 import が使用できない。ということで挙動については腑に落ちた。
さらにここからの帰結として、例に挙げた a/b/c.py
のように、スクリプトとしてもモジュールとしても使用したいコードから import されるモジュールは、pip install
する必要がある。そうすれば常に絶対 import が使用できるため、どちらの方法によっても使用できる。
未解決問題
pytest の起動方法によって import の挙動が異なることも確認できた。
テストスクリプトに print 文を入れて試した (-s
は標準出力を pytest に捕捉させないためのオプション)
-
pytest -s test/<テストファイル.py>
とするとsys.path
の先頭はtest
ディレクトリで、あとに続くのはシステムのデフォルト値 - だが、
python -m pytest -s test/<テストファイル.py>
とするとtest
ディレクトリのあとに空文字列""
が追加されている。おそらくこれはカレントディレクトリを意味すると思われる。
なぜこうなるかについても一部は判明。Python インタプリタ起動時に -m
でモジュールを指定した場合、カレントディレクトリが sys.path
の先頭に追加される。この場合 test
を sys.path
に追加しているのはインタプリタではなくて pytest かもしれない。なぜならこの場合、-s test/<テストファイル.py>
はたんなる引数であり、実行されるスクリプトの名前ではないからである [要調査]。
また、pip でコマンドをインストールした際に、そのコマンドを起動した場合の sys.path
などの設定についても [要確認]。
さいごに
間違いの指摘など、歓迎です。