delhi09の勉強日記

技術トピック専用のブログです。自分用のメモ書きの投稿が多いです。あくまで「勉強日記」なので記事の内容は鵜呑みにしないでください。

Pythonのモジュールのimportの仕組みについて勉強する

Pythonのモジュールのimportの仕組みについて、ちゃんと理解していなかったので勉強した。

1.Pythonがモジュールを検索するパス

Pythonsys.pathというリスト型の変数に含まれるパスを起点にモジュールを検索する。

詳細は以下の公式ドキュメントを参照

docs.python.org

docs.python.org

2.sys.pathの仕様

上記の公式ドキュメントによると、sys.pathには以下が登録される。

例えば、/var/path/to/bbb/var/path/to/cccPYTHONPATHに登録した後、/var/tmp/path/to/aaa/dump_syspath.pyを実行して、sys.pathの内容を出力した時の結果は以下のようになる。

PYTHONPATHを宣言する。
(複数のパスを設定する場合はコロンで区切る。)

$ export PYTHONPATH="/var/tmp/path/to/bbb:/var/tmp/path/to/ccc"

dump_syspath.pyの中身

import sys

for path in sys.path:
    print(path)

・実行結果

$ python /var/tmp/path/to/aaa/dump_syspath.py
/private/var/tmp/path/to/aaa
/tmp/path/to/bbb
/tmp/path/to/ccc
/Library/Frameworks/Python.framework/Versions/3.7/lib/python37.zip
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload
/private/var/tmp/path/to/aaa/.venv/lib/python3.7/site-packages
$

/private/var/tmp/path/to/aaaとなっているのは、Macでは/var/tmp/private/var/tmpへのシンボリックリンクになっているからである。
apple.stackexchange.com

また、virtualenvの仮想環境もパスに登録されていることが分かる。

3.__init__.pyはPython3.3以降では必須ではなくなった。

Pythonのモジュールのimportについて検索すると、pythonファイルをモジュールとして認識させるためには、該当のpythonファイルが存在するディレクトリに__init__.pyというファイルを配置する必要があるという記事をよく見かける。

また、公式ドキュメントの日本語訳にも「ファイルを含むディレクトリをパッケージをとしてPython に扱わせるには、ファイル __init__.py が必要です。 」と記載されている。(2020/6/2時点)

docs.python.org

しかし、Python3.3以降では、__init__.pyは必須ではなくなったようである。

理由は、pep-0420の仕様が実装されたことにより、__init__.pyが存在しなくても、import文に宣言されているモジュール名と名前が同一のファイル or ディレクトリが存在するだけで、モジュールとして認識されるようになったからとのことである。

www.python.org

以下が仕様が明記されている箇所だと思われる。

  • If /foo/__init__.py is found, a regular package is imported and returned.
  • If not, but /foo.{py,pyc,so,pyd} is found, a module is imported and returned. The exact list of extension varies by platform and whether the -O flag is specified. The list here is representative.
  • If not, but /foo is found and is a directory, it is recorded and the scan continues with the next directory in the parent path.
  • / foo / __ init__.pyが見つかった場合、通常のパッケージがインポートされて返されます。
  • 見つからないが、 / foo.{py、pyc、so、pyd}が見つかった場合、モジュールがインポートされて返されます。拡張子の正確なリストは、プラットフォームおよび-Oフラグが指定されているかどうかによって異なります。ここのリストは代表的なものです。
  • そうでない場合でも、 / fooが見つかり、それがディレクトリーである場合、それが記録され、スキャンは親パスの次のディレクトリーから続行されます。

(Google翻訳)

実際に、Python3.7系とPython3.2系で検証してみた。

構成

/tmp/path/to/aaa
├── hello_module
│   └── hello.py
└── main.py
$

[main.py]

from hello_module.hello import hello

hello()

[hello_module/hello.py]

def hello():
    print("Hello World!")

検証方法

Python3.7系とPython3.2系の公式のDockerコンテナでそれぞれmain.pyを実行してみる。

Python3.7系

$ pwd
/tmp/path/to/aaa
$ docker pull python:3.7.7
$ docker run -it --rm --name my-running-script -v "$PWD":/usr/src/myapp -w /usr/src/myapp python:3.7.7 python main.py
Hello World!

→ 正常に実行できた。

Python3.2系

$ pwd
/tmp/path/to/aaa
$ docker pull python:3.2.6
$ docker run -it --rm --name my-running-script -v "$PWD":/usr/src/myapp -w /usr/src/myapp python:3.2.6 python main.py
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    from hello_module.hello import hello
ImportError: No module named hello_module.hello

→ 「ImportError: No module named hello_module.hello」が発生した。

・空の__init__.pyを配置すると正常に実行できるようになった。

$ touch hello_module/__init__.py
$ docker run -it --rm --name my-running-script -v "$PWD":/usr/src/myapp -w /usr/src/myapp python:3.2.6 python main.py
Hello World!

4.実例

4-1.起動スクリプトの同一階層にモジュールが存在する場合

以下のような場合を例とする。

構成
/tmp/path/to/aaa
├── hello.py
└── main.py

[main.py]

from hello import hello

hello()

[hello.py]

def hello():
    print("Hello World!")

実行結果

$ python main.py
Hello World!
$

/tmp/path/to/aaasys.pathに登録されているため、正常に実行できる。

4-2.起動スクリプトの下の階層にモジュールが存在する場合

以下のような場合を例とする。

構成
/tmp/path/to/aaa
├── hello.py
└── main.py

[main.py]

from module_dir.hello import hello

hello()

[module_dir/hello.py]

def hello():
    print("Hello World!")

実行結果

$ python main.py
Hello World!
$

Pythonのモジュール検索の仕様により、sys.pathに登録されている/tmp/path/to/aaa配下のmodule_dirディレクトリを検索するため、この場合も正常に実行できる。

4-3.起動スクリプトとモジュールがそれぞれ別々のディレクトリに存在する場合

以下のような場合を例とする。

/tmp/path/to/aaa
├── main_dir
│   └── main.py
└── module_dir
    └── hello.py

[main_dir/main.py]

from module_dir.hello import hello

hello()

[module_dir/hello.py]

def hello():
    print("Hello World!")

実行結果

$ cd main_dir
$ python main.py
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    from module_dir.hello import hello
ModuleNotFoundError: No module named 'module_dir'
$

→ この場合、sys.pathに登録されるのは/tmp/path/to/aaa/main_dirとなるため、module_dir/hello.pyは検索対象に含まれない。

従って、「ModuleNotFoundError: No module named 'module_dir'」が発生してエラーとなる。

以下のように、PYTHONPATH/tmp/path/to/aaaを登録すると正常に実行されるようになる。

$ export PYTHONPATH=/tmp/path/to/aaa
$ python main.py
Hello World!
$

以上