かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

Python3 自作モジュールのインポートにハマる

Python3 (3.6.5)でオレオレモジュールを作成してインポートしようとした際に結構ハマったのでメモ

importの基本

import <module>キーワードでモジュールをインポートする

import math
print( math.pi )
# => 3.141592653589793

モジュールのメソッドはmodule.methodで実行する

from <module> import <method> でインポートするとモジュール名を都度記述しなくてもメソッドの呼び出しができる

from math import pi, sqrt
print( sqrt(9) )
# => 3.0

from ... import ...でインポートする際にメソッド名が被ると、後からインポートしたものが有効になる

from math import pi
from my_modules import pi
pi # => my_modules.pi 

一度インポートされたモジュールは再インポートされない

モジュール echo.py

# echo.py
print( f'echo.py: name is {__name__}' )

実行ファイル base.py

# base.py
import echo
# => echo.py が実行される
print('---')
import echo
# => 再実行はされないので何も出力されない

実行↓

$ python base.py
echo.py: __name__ is echo
---

同じ階層にあるディレクトリ内のファイルをインポート

フォルダ構成

/root
  |- /modules
  |    |- echo.py
  |- base.py
# modules/echo.py
def say_name():
  print("I'm " + __name__)

import <ディレクトリ>.<ファイル名>でインポート

# base.py
import modules.echo
modules.echo()
# => I'm modules.echo

from <ディレクトリ> import <ファイル名>でインポート

# base.py
from modules import echo
echo.say_name()
# => I'm modules.echo

import <ディレクトリ>.<ファイル名>from <ディレクトリ> import <ファイル名>でのインポートはどの階層からbase.pyを実行しても問題なくインポートすることができるようです。実行位置ではなくファイルから相対パスで読み込まれるという認識で良いのでしょうか?(この部分正確にな挙動はまだ理解できていません)

$ python root/base.py
> "I'm modules.echo"

$ cd root
$ :root python base.py
> "I'm modules.echo"

$ cd modules
$ :modules python ../base.py
> "I'm modules.echo"

更に深い階層にあるファイルのインポート

フォルダ構成

/root
  |- /modules
  |    |- /sub
  |        |- echo.py
  |- base.py

import ... - ディレクトリを.で繋げる

# base.py
import modules.sub.echo
modules.sub.echo.say_name()
# => I'm modules.echo

from ... import ... - fromの後に.区切りでディレクトリを指定する。

# base.py
from modules.sub import echo
echo.say_name()
# => I'm modules.echo

from ... import ...でimportの後にディレクトリを.区切りで表記するとエラー

# base.py
from modules import sub.echo
# =>    from modules import sub.echo
# =>                           ^
# => SyntaxError: invalid syntax

実行ファイルの上の階層にあるファイルのインポート

フォルダ構成

.
|- /root
|    |- base.py
|- /modules
|    |- echo.py

Pythonでは実行ファイルより上の階層にアクセスできない!?

モジュールのインポートで階層を.で表しているのだと思い、次のようにしてみた所エラーになってしまいました。

# base.py
from ..modules import echo
# => ValueError: attempted relative import beyond top-level package
# base.py
import ..modules.edho
# => SyntaxError: invalid syntax

どうやらPythonは現在実行されているファイルをモジュールのルートとみなすという仕様があり、その結果ルートより上の階層が見えなくなっていてるためエラーになるようです。

sys.path にモジュールのあるパスを追加するとインポートできる

sys.pathにはモジュールを検索するパスのリストが入っており、ここにインポートしたいファイルのあるパスを追加しておけば、実行ファイルの上の階層であってもインポートすることが出来るようになるみたいです。

# root/base.py
import sys
sys.path.append('../modules')
print(sys.path)
import echo

どうやらsys.pathにあるパスは実行した位置を起点にして探すようで、上記のように実行ファイルからの相対パスで指定した場合/rootディレクトリ以外で実行するとエラーになってしまいます。

$ python root/base.py
> ['/Users/.../root', '/User/.../.pyenv/versions/3.6.5/lib/python3.6', ... , '../modules']
  # ↑ sys.path に '../modules' がそのまま入っている
> ...
>     import echo
> ModuleNotFoundError: No module named 'echo'

パスの問題を解決するには、環境依存にならないよう動的に絶対パスを取得する方法があれば良さそうです。

pathlib でモジュールの絶対パスを取得する

Python 3.4 以上ではpathlibを使って絶対パスを取得するのが良さそうです。

# root/base.py
import sys
import pathlib
# base.pyのあるディレクトリの絶対パスを取得
current_dir = pathlib.Path(__file__).resolve().parent
# モジュールのあるパスを追加
sys.path.append( str(current_dir) + '/../' )

# モジュールのインポート
import echo
echo.say_name()
# => I'm echo

pathlibで取得できるパスはpathlib.PosixPathオブジェクトなので、そのままパスを編集しようと文字列連結しようとするとエラーになってしまいます。文字列連結などでパスを編集する際にはstr()で文字列にする必要があるのがハマりどころかもしれません。
これで実行ファイルの上の階層のファイルも読み込めるようになりました。

 
node.jsやrubyphpにくらべて、自分の位置の絶対パスの取得や上の階層のファイルのインポートが面倒だな〜という印象を持ちました。何かしら意味があって実行ファイルをrootとして上の階層にアクセスできなくしている経緯があると思うのですが、そのあたり詳しく説明されているサイトを見つけることが出来なかったので、少しもやっとしています。
それと、上の階層のファイルを読み込むためにsys.path にモジュールのあるディレクトリのパスを渡すという方法がお作法的に良いのかどうかいまいち判断できていません。
お作法的なものがあれば知りたいです。 (このドキュメント読めとかあればお願いします。)

また__init__.pyなどでディレクトリ内のモジュールをまとめてインポートとか出来るようなので、そのあたりは追々試してしていきたいと思います。


[参考]

ペンギン・ハイウェイ (角川文庫)

ペンギン・ハイウェイ (角川文庫)

森見登美彦作品好きなので映画化うれしい。