Pythonのモジュールのimportの仕組みについて勉強する
Pythonのモジュールのimportの仕組みについて、ちゃんと理解していなかったので勉強した。
2.sys.path
の仕様
上記の公式ドキュメントによると、sys.path
には以下が登録される。
例えば、/var/path/to/bbb
、/var/path/to/ccc
をPYTHONPATH
に登録した後、/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時点)
しかし、Python3.3以降では、__init__.py
は必須ではなくなったようである。
理由は、pep-0420
の仕様が実装されたことにより、__init__.py
が存在しなくても、import文に宣言されているモジュール名と名前が同一のファイル or ディレクトリが存在するだけで、モジュールとして認識されるようになったからとのことである。
以下が仕様が明記されている箇所だと思われる。
- 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/aaa
がsys.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! $
以上