CLOVER🍀

That was when it all began.

Pythonのimportがよくわからなかったので、調べてみる

これは、なにをしたくて書いたもの?

Pythonのプログラムを読み書きしていて、importがよくわからなかったので、ちゃんと見てみることにしました。

importとモジュールとパッケージ

import自体については、こちらのページを参照。

5. インポートシステム — Python 3.8.10 ドキュメント

importは、あるPythonモジュールから別のPythonモジュールを使うための仕組みです。

さらにパッケージというものもあり、以下の説明を参照すると、ざっくりモジュール=ファイル、パッケージ=ディレクトリと
という捉え方で良さそうです(厳密にはそうではないと書かれてはいますが)。

パッケージはファイルシステムのディレクトリ、モジュールはディレクトリにあるファイルと考えることができますが、パッケージやモジュールはファイルシステムから生まれる必要はないので、この比喩を額面通りに受け取ってはいけません。この文書の目的のために、ディレクトリとファイルという便利な比喩を使うことにします。

import / パッケージ

モジュールは、作成した.pyファイルのこと、という感じですね。つまり、ファイル名がモジュールの名前として
反映されます、と。

パッケージについての説明には続きがあり、通常のパッケージと、名前空間パッケージの2つがあるようです。

すべてのパッケージはモジュールですが、すべてのモジュールがパッケージとは限らないことを心に留めておくのが重要です。もしくは他の言い方をすると、パッケージは単なる特別な種類のモジュールであると言えます。

絶対importと相対import

importには、絶対importと相対importの2つがあるようです。

相対importは、こんな感じに書く書き方みたいです。

from .moduleY import spam
from .moduleY import spam as ham
from . import moduleY
from ..subpackage1 import moduleY
from ..subpackage2.moduleZ import eggs
from ..moduleA import foo

.や..を使った書き方ですね。

import / Package Relative Imports

「現在のパッケージ位置から見た、サブパッケージ(サブディレクトリ)」的な考えのことを言うのかとも思ったのですが、
そういうわけでもなさそうです。

Effective Pythonを見ると、PEP 8に従って絶対importを使うべきだと書かれていました。

絶対import を推奨します。なぜなら、絶対import の方が通常は読みやすく、importシステムが正しく設定されなかった(たとえばパッケージ内部のディレクトリが sys.path で終わっていた) 場合でも、より良い振る舞いをする(または少なくともより良いエラーメッセージを出す)からです

PEP 8 / import

確かに、.や..を使うよりも、絶対importで書いた方が明快でしょうね。

今回は、相対importは使わないことにします。

sys.pathとPYTHONPATH

次に、Pythonプログラムで、モジュールをどこから探すかについて。

こちらに関する話題が、sys.pathとPYTHONPATHのようです。

まずは、sys.pathの説明から。Pythonインストール時の値と、PYTHONPATH環境変数で検索パスが決まるようです。

モジュールを検索するパスを示す文字列のリスト。 PYTHONPATH 環境変数と、インストール時に指定したデフォルトパスで初期化されます。

sys --- システムパラメータと関数 / sys.path

さらに、その先頭には起動スクリプトが配置されているディレクトリが入るようです。

起動時に初期化された後、リストの先頭 (path[0]) には Python インタプリタを起動したスクリプトのあるディレクトリが挿入されます。スクリプトのディレクトリがない (インタプリタが対話セッションで起動された時や、スクリプトを標準入力から読み込んだ場合など) 場合、 path[0] は空文字列となり、Python はカレントディレクトリからモジュールの検索を開始します。

次に、PYTHONPATHについて。こちらは、sys.pathにモジュールの検索パスを追加できる環境変数のようです。

モジュールファイルのデフォルトの検索パスを追加します。この環境変数のフォーマットはシェルの PATH と同じで、 os.pathsep (Unix ならコロン、 Windows ならセミコロン) で区切られた1つ以上のディレクトリパスです。存在しないディレクトリは警告なしに無視されます。

コマンドラインと環境 / PYTHONPATH

この結果、sys.pathは

で構成されるなるようです。

スクリプトディレクトリは、 PYTHONPATH で指定したディレクトリの 前 に挿入されますので注意が必要です。

いずれにせよ、起動スクリプトが配置されているディレクトリが最も前に来るということですね。

また、PYTHONPATHについては、PYTHONHOMEの内容がデフォルトで追加されるようです。

デフォルトの検索パスはインストール依存ですが、通常は prefix/lib/pythonversion で始まります。 (上の PYTHONHOME を参照してください。) これは 常に PYTHONPATH に追加されます。

PYTYHONHOMEは、

コマンドラインと環境 / PYTHONHOME

これがsys.pathのどこに入るかというと、Pythonライブラリのインストール先でprefix/lib/pythonversionと
exec_prefix/lib/pythonversionが対象になります。

ここまでを踏まえて、sys.pathの内容がモジュール検索のルールを理解するのに重要になってくるのかな、と思います。

いろいろ情報を調べたところで、今度は実際に試して確認してみましょう。

環境

今回の環境は、こちらです。

$ python3 -V
Python 3.8.5

いくつかパターンを交えながら、importの確認をしていきたいと思います。

sys.pathを確認する

最初に、sys.pathを確認してみましょう。

One Liner。

$ python3 -c 'import sys; print(sys.path)'
['', '/usr/lib/python38.zip', '/usr/lib/python3.8', '/usr/lib/python3.8/lib-dynload', '$HOME/.local/lib/python3.8/site-packages', '/usr/local/lib/python3.8/dist-packages', '/usr/lib/python3/dist-packages']

最初が空文字なのは、sys.pathのドキュメントに説明があります。この場合、カレントディレクトリが探索パスに
なるようですね。

スクリプトのディレクトリがない (インタプリタが対話セッションで起動された時や、スクリプトを標準入力から読み込んだ場合など) 場合、 path[0] は空文字列となり、Python はカレントディレクトリからモジュールの検索を開始します。

sys --- システムパラメータと関数 / sys.path

次に、スクリプトファイルを用意してみます。

$ echo 'import sys; print(sys.path)' > run.py

$ python3 run.py 
['/path/to/script-location-directory', '/usr/lib/python38.zip', '/usr/lib/python3.8', '/usr/lib/python3.8/lib-dynload', '$HOME/.local/lib/python3.8/site-packages', '/usr/local/lib/python3.8/dist-packages', '/usr/lib/python3/dist-packages']

この場合、スクリプトファイルが配置されたディレクトリがsys.pathの先頭に入ります。

なので、こんな感じにサブディレクトリにスクリプトを配置して確認すると、あくまでスクリプトが配置されている
ディレクトリが起点になっていることが確認できます。

$ mkdir sample && echo 'import sys; print(sys.path)' > sample/run.py

$ python3 sample/run.py 
['/path/to/current-directory/sample', '/usr/lib/python38.zip', '/usr/lib/python3.8', '/usr/lib/python3.8/lib-dynload', '$HOME/.local/lib/python3.8/site-packages', '/usr/local/lib/python3.8/dist-packages', '/usr/lib/python3/dist-packages']

これで、sys.pathの雰囲気はわかりました。

PYTHONPATHを設定してみる

次に、PYTHONPATHを設定して、先ほどのスクリプトを実行してみましょう。

試しに、/tmpと/varを追加してみます。

$ PYTHONPATH=/tmp:/var python3 run.py 
['/path/to/script-location-directory', '/tmp', '/var', '/usr/lib/python38.zip', '/usr/lib/python3.8', '/usr/lib/python3.8/lib-dynload', '$HOME/.local/lib/python3.8/site-packages', '/usr/local/lib/python3.8/dist-packages', '/usr/lib/python3/dist-packages']


$ PYTHONPATH=/tmp:/var python3 sample/run.py
['/path/to/current-directory/sample', '/tmp', '/var', '/usr/lib/python38.zip', '/usr/lib/python3.8', '/usr/lib/python3.8/lib-dynload', '$HOME/.local/lib/python3.8/site-packages', '/usr/local/lib/python3.8/dist-packages', '/usr/lib/python3/dist-packages']

確かに、sys.pathの「スクリプトの配置ディレクトリの次の位置」に追加されましたね。

設定方法はわかりました。

パッケージを使ったスクリプトを作成する

次に、パッケージを使ったスクリプトを作成してみましょう。

こんな感じのディレクトリ構成を用意。

.
├── main.py
├── sub.py
├── one
│   └── say.py
└── two
    └── calc.py

パッケージ内には、main.pyおよび別パッケージのモジュールを呼び出すスクリプトを用意。

one/say.py

from two import calc

def hello():
    print('Hello World!! from package one')

def plus(a, b):
    return calc.plus(a, b)

two/calc.py

def plus(a, b):
    return a + b

起動スクリプトと同じディレクトリにも、モジュールを用意。

sub.py

def message():
    print('Hello World!! from main directory')

起動スクリプトは、パッケージ内のモジュールを呼び出し、最後にsys.pathの内容を表示します。

main.py

import sys

from one import say
import sub

if __name__ == '__main__':
    sub.message()
    say.hello()
    print('1 + 3 = ' + str(say.plus(1, 3)))
    
    print()

    print('print sys.path:')

    for path in sys.path:
        print(f'  {path}')

実行してみましょう。

$ python3 main.py 
Hello World!! from main directory
Hello World!! from package one
1 + 3 = 4

print sys.path:
  /path/to/script-location-directory
  /usr/lib/python38.zip
  /usr/lib/python3.8
  /usr/lib/python3.8/lib-dynload
  $HOME/.local/lib/python3.8/site-packages
  /usr/local/lib/python3.8/dist-packages
  /usr/lib/python3/dist-packages

こんな感じに実行できました。

ここまでは良いかな、と。

起動スクリプトをパッケージ内に置いた場合

次に、起動スクリプトを含めてパッケージ内に配置した場合。

.
└── app
    ├── main.py
    ├── one
    │   └── say.py
    ├── sub.py
    └── two
        └── calc.py

このappというディレクトリを、パッケージとして扱うことにします。

各ファイルで他のモジュールを使う際のimport文には、appを追加します。

app/main.py

import sys

from app.one import say
from app import sub

if __name__ == '__main__':
    sub.message()
    say.hello()
    print('1 + 3 = ' + str(say.plus(1, 3)))
    
    print()

    print('print sys.path:')

    for path in sys.path:
        print(f'  {path}')

main.pyについては、importのここだけfrom、importを使うように修正しました。

from app import sub

app/sub.py

def message():
    print('Hello World!! from main directory')

app/one/say.py

from app.two import calc

def hello():
    print('Hello World!! from package one')

def plus(a, b):
    return calc.plus(a, b)

app/two/calc.py

def plus(a, b):
    return a + b

すると、途端にこれはうまく動かなくなります。

$ python3 app/main.py
Traceback (most recent call last):
  File "app/main.py", line 3, in <module>
    from app.one import say
ModuleNotFoundError: No module named 'app'

スクリプトが配置されているディレクトリに移って実行しても、うまくいきません。

$ cd app

$ python3 main.py
Traceback (most recent call last):
  File "main.py", line 3, in <module>
    from app.one import say
ModuleNotFoundError: No module named 'app'

どちらもappがわからない、と言っています。

これが最初よくわからなかったのですが、sys.pathとPYTHONPATHの説明を見ていると、なんとなくわかってきます。

sys.pathに追加されるのは、「起動スクリプトが配置されているディレクトリ」であり、今回のような構成だと
appパッケージはsys.pathの外側にいるからですね。

いったん、元のディレクトリに戻って

$ cd ..

今回のケースで、ソースコードを変更せずにそのまま動かしたい場合は、PYTHONPATHにappパッケージが対象に入るように
sys.pathに含めればよい、ということになります。

$ PYTHONPATH=/path/to python3 app/main.py
Hello World!! from main directory
Hello World!! from package one
1 + 3 = 4

print sys.path:
  /path/to/app
  /path/to
  /usr/lib/python38.zip
  /usr/lib/python3.8
  /usr/lib/python3.8/lib-dynload
  /home/kazuhira/.local/lib/python3.8/site-packages
  /usr/local/lib/python3.8/dist-packages
  /usr/lib/python3/dist-packages

ここで、/path/toはカレントディレクトリとします。今回はカレントディレクトリの中にappパッケージが
配置されている、と読んでください。

もしくは、起動スクリプトだけパッケージの外に置く(起動スクリプトと同じ並びにappパッケージを配置する)、
とかでしょうか。

$ mv app/main.py main.py

この場合、起動スクリプトから見たディレクトリ内にappパッケージが含まれるため、モジュールの探索パスに入る、
ということになります。

$ python3 main.py
Hello World!! from main directory
Hello World!! from package one
1 + 3 = 4

print sys.path:
  /path/to/script-location-directory
  /usr/lib/python38.zip
  /usr/lib/python3.8
  /usr/lib/python3.8/lib-dynload
  $HOME/.local/lib/python3.8/site-packages
  /usr/local/lib/python3.8/dist-packages
  /usr/lib/python3/dist-packages

理屈はわかった気がします。

あと、別解として起動スクリプトをappパッケージに置いたまま、起動スクリプトの親ディレクトリをsys.pathに追加する、
みたいな方法もあると思いますが。

app/main.py

import os
import sys

parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(parent_dir)

from app.one import say
from app import sub

if __name__ == '__main__':
    sub.message()
    say.hello()
    print('1 + 3 = ' + str(say.plus(1, 3)))
    
    print()

    print('print sys.path:')

    for path in sys.path:
        print(f'  {path}')

これでも、動くには動きます。

$ python3 app/main.py
Hello World!! from main directory
Hello World!! from package one
1 + 3 = 4

print sys.path:
  /path/to/app
  /usr/lib/python38.zip
  /usr/lib/python3.8
  /usr/lib/python3.8/lib-dynload
  $HOME/.local/lib/python3.8/site-packages
  /usr/local/lib/python3.8/dist-packages
  /usr/lib/python3/dist-packages
  /path/to

が、ふつうのスクリプトだと、あんまりやりたくないですね…。

まとめ

Pythonのimportがよくわからなかったので、調べてみました。

起動スクリプトの位置とパッケージの構成に気をつけないと、簡単にハマるんだなぁ、と。

どうなんでしょうね、PYTHONPATHで調整するのがいいのでしょうか。それとも起動スクリプトはパッケージに含めない方が
楽でしょうか?