4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Python】パッケージ・モジュールの検索と import

Last updated at Posted at 2020-04-12

ChangeLog

  • 2020-04-18: __main__ モジュールにおける振る舞いについて訂正。

概要

なんとなくわかった気になって使っている人が多そうな、Python のパッケージとモジュールについてざっくりと勉強します。
あまりまとまってないのでいつか書き直す。

動機

他人の書いた Python の機械学習のコードがあります。
カレントディレクトリからみて ./a/b/c.py の場所に実行したいファイルがあります。
このファイルは __name__ の値によって2通りに動作する、よくある形式で書かれています。
中では ./a/b/d.pyimport 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 <ファイル> で起動されるファイルのことを スクリプト と読んでいることに注意する。

インポートする側

いちばん単純には

  • import 文 import mモジュール m をインポートしている
  • モジュール m とは m.py という Python ファイル である
  • これにより、m.py 内で定義されている 名前 (変数名、関数名、クラス名、など) nm.n でアクセスできる

と理解する。

  • 実際にはモジュール名に加えて、パッケージパスを含める必要がある場合が多い (後述)
  • 実際にはパッケージも import できる (後述)

インポートされる側

  • 自分自身のモジュール名は __name__ で参照できる。上記の方法でインポートされた m の中では、__name__"m" になる
  • ただし、自分がトップレベルであるとき、つまり自モジュールが python コマンドの引数で指定されたファイルの場合、__name__"__main__" になる。トップレベル環境は __main__ モジュールとして動作している、と考えるとよい
  • モジュールは他のモジュールをインポートできる

モジュール探索パス (sys.path)

上記の例で、import 対象のモジュール m、すなわち、ファイル m.pysys.path (sys モジュールの path 変数の値) に設定されているディレクトリのリストの先頭から順に検索される。デフォルトでは以下の順に並んでいる:

  1. スクリプト のあるディレクトリ、指定されない場合 (python により対話セッションが実行された場合など) はカレントディレクトリ
  2. PYTHONPATH 環境変数の値
  3. インストールごとのデフォルト (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 dfrom . 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 できるのは
    • 名前 nfrom a.b.c import n とすると、以後は n だけで参照できる
    • モジュール c。 先ほど説明したとおり
    • パッケージ bfrom a import b で。以後はパッケージ ba.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 のように相対インポートに書き換えると、cimport 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 の先頭に追加される。この場合 testsys.path に追加しているのはインタプリタではなくて pytest かもしれない。なぜならこの場合、-s test/<テストファイル.py> はたんなる引数であり、実行されるスクリプトの名前ではないからである [要調査]。
また、pip でコマンドをインストールした際に、そのコマンドを起動した場合の sys.path などの設定についても [要確認]。

さいごに

間違いの指摘など、歓迎です。

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?