はじめに
Pythonの相対importで今までよく考えずUnitTestで使っていたり,ディレクトリを掘ってわけることを怖がっていたのですが,そろそろ本腰を入れて解決しようと思いいたり,調べたのち解決できたので記録として残しておきます.
attempted relative import beyond top-level packageを再現しよう
以下のようなディレクトリ構成で
/package/
__init__.py
subpackage1/
__init__.py
moduleX.py
moduleY.py
subpackage2/
__init__.py
moduleZ.py
moduleA.py
moduleZ.py が moduleX.py をimportしているとき,つまり moduleZ.py ファイル内で
from ..subpackage1 import moduleX
と書いて
$ pwd
/package
$ cd subpackage2
$ python moduleZ.py
ValueError: attempted relative import beyond top-level package
## or
$ python subpackage2/moduleZ.py
ValueError: attempted relative import beyond top-level package
と無事再現できます.
解決するためには
ここの2つのリューション[1]がとても良い解決策を提示していたので使用しました.
今回は
moduleZ.py のimportを
from subpackage1 import moduleX
と書き換えて
$ pwd
/package
$ python -m subpackage2.moduleZ
で解決する方法をとります.
-m はライブラリモジュールとしてpythonスクリプトとして実行します.なので,/package をrootディレクトリ(__main__) としてsubpackage2/moduleZ.py を実行しているため,subpackage1/moduleX.py をimportして実行することができます.
Pythonのimportに対する考え方
PythonはPackageの考え方[2]はpython ~~~.pyを実行した場所をrootとして認識しroot以下に存在する~~~.pyをモジュール(~~~.py)として認識しています.このrootとして認識される~~~.pyスクリプトは1つだけです.さらに,python ~~~.pyとスクリプトを実行するとPythonの__main__がimportの特殊ケース[3]によってモジュール名が強制的に__main__となります.したがって,どのPythonスクリプトをroot(__main__)に実行しているのか実行ディレクトリの場所が大切になります.
今回の失敗例では以下のようにroot(__main__)はmoduleZ.pyの場所(今回はsubpackage2)となってしまいます.
$ pwd
/package
$ cd subpackage2
$ python moduleZ.py
## or
python subpackage2/moduleZ.py
その結果,相対importで(subpackage1)をimportする際,root(subpackage2)の上(/pacakge)を参照しようとしてValueError: attempted relative import beyond top-level packageと怒られPythonスクリプトは実行できません.
ちなみにpythonで始まるREPLはカレントディレクトリが__main__となってるのでrootディレクトリで実行するとimportができます.
逆にどこかのパッケージ(ディレクトリ)内で実行してしてしまうとimportできない場合があります.
最後に
Pythonのimportはわかったようなわからないような不思議な感覚で使っていたので割と整理できて愛着がわきました.身近なものだとPythonのUnitTestでpython -mを使っている方は多いのではないでしょうか?調べ不足で私の誤認やタイプミスがありましたら,コメントに記載していただけると幸いです.また,この記事でPythonインポート正直理解できないと思われる方はぜひ参考資料も見ていただけると嬉しいです.
参考資料
[1] Relative imports for the billionth time
[2] Built-in Package Support in Python 1.5
[3] 5.8. main に対する特別な考慮