かざんぶろぐ

だらだらと備忘録とってます

Pythonで作成した自作モジュールを様々な階層からimport

作成したPythonファイルをモジュールとして、他のPythonファイルから呼び出し(import)、1つのファイルを複数のファイルから再利用することができる。
今回はそのモジュールのimportに関する備忘録。

パッケージとモジュールと__init__.py

一応メモしておくと、モジュールが.pyファイルなのに対し、パッケージは複数のモジュールがまとまったディレクトリのことを指すらしい。
ここで注意したいのは、パッケージとなるディレクトリには__init__.pyというファイルを置かなくてはならないこと。
__init__.pyとは、モジュールをimportする時の初期化を行ってくれるファイルであり、このファイルが置いていないパッケージのモジュールをimportしようとしても、import errorとなってしまう。
あくまで、「このファイルにはモジュールが存在する」ということを表すだけなので、今回はこのファイルの中身は空である。

環境

Mac Book Air : OS X EI Caption(version 10.11.6)
Python 2.7.12 |Anaconda custom (x86_64)|

前準備

Parentsディレクトリ内に、
__init__.py , main.py , parent_main.py , parent_module.pyの4つのファイルと、
Childrenディレクトリを作成し、
Childrenディレクトリ内に、
__init__.py , child_main.py , child_module.pyの3つのファイルを作成した。

Parentsディレクトリ
      |
      ---__init__.py
      |
      ---main.py
      |
      ---parent_main.py
      |
      ---parent_module.py
      |
      ---Childrenディレクトリ
                  |
                  ---__init__.py
                  |
                  ---child_main.py
                  |
                  ---child_module.py



parent_module.py

def goal():
    print "I am parent."



child_module.py

def goal():
    print "I am child."

やったこと

  1. main.pyからparant_moduleをimport(同じ階層からの呼び出し)
  2. parent_main.pyからchild_moduleをimport(上の階層からの呼び出し)
  3. child_main.pyからparent_moduleをimport(下の階層からの呼び出し)

1.同じ階層からの呼び出し

Parentsディレクトリ
      |
      ---main.py
      |
      ---parent_module.py

同じディレクトリに存在するモジュールをimportする


main.py

import parent_module
parent_module.goal()



結果

$ python -B main.py
I am parent

※オプションで-Bをつけることで、モジュールの.pycファイルができなくなる。


同じ階層の場合は上記の通り、importしたいファイルのファイル名から.pyを抜いたものを記入すればOK。

2.上の階層からの呼び出し

Parentsディレクトリ
      |
      ---parent_main.py
      |
      ---Childrenディレクトリ
                  |
                  ---child_module.py

1つ下のディレクトリに存在するモジュールをimportする


parent_main.py

import Children.child_module
Children.child_module.goal()

または

from Children.child_module import goal
goal()



結果

$ python -B parent_main.py
I am child



fromを使う場合と使わない場合の違いについては、この記事では省略する。
注意すべき点としては、fromを使った場合は、importの後にaaa.bbbのように「.」を使うことはできない。
そして以下のようなコードはエラーが出る

import Children.child_module.goal
Children.child_module.goal()

3.下の階層からの呼び出し

Parentsディレクトリ
      |
      ---parent_module.py
      |
      ---Childrenディレクトリ
                  |
                  ---child_main.py

1つ上のディレクトリに存在するモジュールをimportする。
まずはエラーが出る例について示す。


child_main.py

from .. import parent_module
parent_module.goal()

このようにコードを書くと、以下のようなエラーメッセージが出ていてしまう。

ValueError: Attempted relative import in non-package



ここで、child_main.pyに以下のようなコードを書いて実行してみる。

import sys
print sys.path



結果

$ python Children/child_main.py
['/Users/okuyamatakashi/Parents/Children', 環境変数で設定しているpath達]

sys.pathは絶対パスの文字列リストであり、リスト内には、実行スクリプトが存在するディレクトリの、絶対パスが加わっている。
ポイントは、「sys.pathより上はパッケージの検索対象ではない」という点だ。
なので解決方法としては、
「sys.pathに上の階層にあるパッケージ(Parents)の絶対パスを追加して検索対象にする」といった感じ。

import sys
import os

print "os.getcwd() -> ",os.getcwd()

sys.path.append(os.getcwd())

import parent_module
parent_module.goal()



結果

$ python Children/child_main.py
os.getcwd() ->  /Users/okuyamatakashi/Parents
I am parent!!



os.getcwd()で実行パス(Parentsディレクトリ)を取得し、それをsys.pathに追加することでParentsディレクトリを検索対象にしている。
注意すべき点としては、今回の実行パスがParentsディレクトリであるということ。
これがもしChildrenディレクトリならまた違った方法でParentsディレクトリのパスをsys.pathに加える必要がある。


ここまで書いておいてあれなんだが、sys.pathにパスを追加して無理矢理別階層のモジュールをインポートしたりすることは、インポートされる側が把握しきれなくなったりのであまり良くない。
sys.pathは全てのモジュールが参照するため、他のモジュールに影響を及ぼす可能性があるらしい。
(例えば、あるモジュールを外すと今まで動いていたモジュールが、途端にimport Errorが発生する、挙動が変わってしまうなどの不具合)
解決法としては、
環境変数PYTHONPATHを設定する方法がある。
以下のように環境変数を設定しすると、sys.pathには常にPatentsへのパスが入る。

f:id:okuya-KAZAN:20170624013450p:plain

よってchild_mainの中身は、

import parent_module
parent_module.goal()

これだけで良い。

まとめ

色々なところで使いたいモジュールを置いているパッケージが固定で、そのモジュールを様々なところからimportしたいのであれば、PYTHONPATHを設定すると楽かも。