はじめに
Pythonで自作モジュールをimportする際に以下のようなエラーに引っかかることがあります。
ValueError: attempted relative import beyond top-level package
ImportError: attempted relative import with no known parent package
ModuleNotFoundError
これらを解決するために色々調査をし、自分の中で腹落ちしたので備忘も込めて書き残します。
個人的には以下の理解が深まりました。
キーワード
- モジュール検索パス(sys.path)
- 相対パスimport
環境
- Python 3.8.10
前提知識
import文のおさらい
importとはモジュールを取り込むための構文です。では、モジュールとは何か?
モジュールは Python の定義や文が入ったファイルです。ファイル名はモジュール名に接尾語 .py がついたものになります。(python公式ドキュメントから引用)
非常にシンプルな話で、.py
拡張子のファイル自体をモジュールと呼ぶってことですね。で、モジュール名はファイル名そのものだと。例えば、ファイル名がmod.py
ならモジュール名はmod
になる、ということです。
以下のような構成で、python main.py
を実行すると、this is func of mod.py
が出力されます。
~/ ←カレントディレクトリ
main.py
mod.py
def func():
print("this is func of mod.py")
import mod
mod.func()
from句のおさらい
importは、インポート対象のファイルの中身全てを取り込みます。対してfromは、一部の関数のみ取り込みたいといった場合に使用します。
以下、mod.pyのfunc_Aのみをインポートする例です。func_Bはインポートされてないので、実行できません。
~/ ←カレントディレクトリ
main.py
mod.py
def func_A():
print("this is func_A of mod.py")
def func_B():
print("this is func_B of mod.py")
from mod import func_A
func_A() # 正常に実行される
func_B() # 実行時エラー
パッケージのおさらい
パッケージ (package) は、Python のモジュール名前空間を "ドット付きモジュール名" を使って構造化する手段です。
(python公式ドキュメントから引用)
例えば以下のようなディレクトリ構造だとします。この場合、sub_module_A.pyはpackage.sub_pack_A
というパッケージに属します。
~/ ←カレントディレクトリ
main.py
package/
module_A.py
module_B.py
sub_pack_A/
sub_module_A.py
sub_pack_B/
sub_module_B.py
エラーを解消するのに必要な知識
ここまでが前提知識になります。ここからは、冒頭述べた、importに関する種々のエラーを解消するにあたって必要になってくる知識を述べます。
モジュール検索パス(sys.path)について
spam という名前のモジュールをインポートするとき、インタープリターはまずその名前のビルトインモジュールを探します。見つからなかった場合は、 spam.py という名前のファイルを sys.path にあるディレクトリのリストから探します。
(python公式ドキュメントから引用)
sys.pathの中身を確認してみます。
import sys
print(sys.path)
main.pyを実行してみると、以下のようなリストが出力されます。
[<pythonの実行パス>, '/usr/lib/python38.zip', '/usr/lib/python3.8', '/usr/lib/python3.8/lib-dynload', '/home/s-kento/work_for_python/module_test/.venv/lib/python3.8/site-packages']
importが実行されると、sys.pathに格納されているパス配下を探索するということになります。
ちなみに、以下のような状況で、sub_module_A.pyを直接pythonコマンドで実行すると、pythonの実行パス
=~/package/sub_pack_A
になります。
~/ ←カレントディレクトリ
main.py
package/
module_A.py
module_B.py
sub_pack_A/
sub_module_A.py
sub_pack_B/
sub_module_B.py
相対パスimportについて
何度も登場させていますが、以下のような状況です。
~/ ←カレントディレクトリ
main.py
package/
module_A.py
module_B.py
sub_pack_A/
sub_module_A.py
sub_pack_B/
sub_module_B.py
sub_module_A.pyからsub_module_B.pyをimportしようとすると、以下のように書けます。
from package.sub_pack_B import sub_module_B
ここで、sub_module_A.pyのパスを基準にして相対パスを使うと、以下のようにも書けます。
from ..sub_pack_B import sub_module_B
Linuxでよくcd ..
するのと同じように、..
は一つ上位の階層を表します。
【本題】自作モジュールのimportで発生するエラーを解消する
ここからは、自分がハマッてしまったエラーに対して、その解決方法を書いていきます。
ValueError: attempted relative import beyond top-level package
以下のような状況で、module_A.pyでmodule_B.pyをインポートしたいとします。
~/ ←カレントディレクトリ
main.py
package_A/
module_A.py
package_B/
module_B.py
package_Bはmodule_A.pyの上位ディレクトリだから・・・と思って以下のように書き、main.pyを実行すると、module_A.pyのimport文でValueError: attempted relative import beyond top-level package
が発生します。
from ..package_B import module_B
from package_A import module_A
エラーメッセージの通りなのですが、相対パスimportではトップレベルのパッケージを超えた上位ディレクトリは参照できないということです。どういうことか?
前節で述べたパターンは、こんな感じでsub_package_A
配下からpackage
配下に遡った形ですが、
package.sub_package_A.sub_module_A
↑<============↑
エラーが発生するパターンは、こんな感じでトップディレクトリであるpackage_A
を跨ごうとしてるんですね。
pythonの仕様的にこれはやってはいけねえぞ、ということです。
package_A.module_A
↑<=======↑
解決策
そのまんまですが、..
はいらないということです。
from package_B import module_B
ImportError: attempted relative import with no known parent package
以下のような構成で、sub_module_A.pyからsub_module_B.pyをimportしようとします。今度はmain.pyではなくsub_module_A.pyを直接実行するとImportError: attempted relative import with no known parent package
が発生します。
~/ ←カレントディレクトリ
main.py
package/
module_A.py
module_B.py
sub_pack_A/
sub_module_A.py
sub_pack_B/
sub_module_B.py
from ..sub_pack_B import sub_module_B
main.pyを実行した時と何が違うかというと、sys.pathの中身が違います。sub_module_A.pyを実行するとsys.pathには~/package/sub_pack_A
が入ります。
なので、sub_module_A.pyはpythonを実行しているルートディレクトリ上にいることになり、pythonの仕様上それより上位は遡れねぇぞ、ということです。
ModuleNotFoundError
上の状況に対して、..
を取り除き以下のように書くと、今度はModuleNotFoundError
が発生します。相対パスimportに対するエラーではなくなり、単純にsub_pack_Bが見つからねぇぞ、というエラーです。sys.pathには~/package/sub_pack_A
が入っており、sub_package_A
配下にsub_package_B
はいないので、見つからないのは当然ですね。
from sub_package_B import module_B
解決策
2通りあります。
-mオプションを使用する
pythonコマンドを実行する際、-m
オプションを使用します。このオプションは、モジュール名を指定することによりスクリプトを実行するオプションです。
上の状況において、python package/sub_pack_A/sub_module_A.py
とpython -m package.sub_pack_A.sub_module_A
とで何が違うかと言うと、pythonの実行ディレクトリが異なります。
前者は~/package/sub_pack_A/
なのに対し、後者は~/
になります。その結果、後者はpackage.sub_pack_A.
とpackage.sub_pack_B
がパッケージとして認識されるようになり、相対パスimportも可能になります。
sys.pathにパスを加える
sys.pathに上位ディレクトリを追加します。具体的にはこんな感じです。
import sys
sys.path.append("~/package")
from sub_pack_B import sub_module_B
これで、import実行時に/package
ディレクトリ配下も探索してくれるので、sub_pack_B
が見つかるようになります。
おわりに
モジュール単体を実行する時ってどんな時かと言うと、自分の場合は単体テストをしている時でした。
pytest
で単体テストを作成中、importエラーで小一時間詰まったので、本記事を書くに至りました。
調査がちょっと雑なので、もしかしたら間違ってる箇所があるかもしれませんが、お気づきの際はご指摘もらえると大変ありがたいです。
(自分で見返してみても日本語が分かりにくい箇所がちょいちょいあるので、随時修正していこうと思ってます。。)
あと、解決策2種類しか書いてませんが、たぶんカレントディレクトリ変える方法とかもあるのではと思ってます。
ただそこは調査できてないので、余力があれば調べて追記したいと思います。
以上です!ご精読ありがとうございました!