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で調敎するのがいいのでしょうか。それずも起動スクリプトはパッケヌゞに含めない方が
楜でしょうか