112
66

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Pythonの相対パスimportを理解する

Posted at

はじめに

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
mod.py
def func():
  print("this is func of mod.py")
main.py
import mod
mod.func()

from句のおさらい

importは、インポート対象のファイルの中身全てを取り込みます。対してfromは、一部の関数のみ取り込みたいといった場合に使用します。
以下、mod.pyのfunc_Aのみをインポートする例です。func_Bはインポートされてないので、実行できません。

~/  ←カレントディレクトリ
 main.py
 mod.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")
main.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の中身を確認してみます。

main.py
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しようとすると、以下のように書けます。

sub_module_A.py
from package.sub_pack_B import sub_module_B

ここで、sub_module_A.pyのパスを基準にして相対パスを使うと、以下のようにも書けます。

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が発生します。

module_A.py
from ..package_B import module_B
main.py
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
↑<=======↑

解決策

そのまんまですが、..はいらないということです。

module_A.py
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
sub_module_A.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はいないので、見つからないのは当然ですね。

module_A.py
from sub_package_B import module_B

解決策

2通りあります。

-mオプションを使用する

pythonコマンドを実行する際、-mオプションを使用します。このオプションは、モジュール名を指定することによりスクリプトを実行するオプションです。
上の状況において、python package/sub_pack_A/sub_module_A.pypython -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に上位ディレクトリを追加します。具体的にはこんな感じです。

sub_module_A.py
import sys
sys.path.append("~/package")
from sub_pack_B import sub_module_B

これで、import実行時に/packageディレクトリ配下も探索してくれるので、sub_pack_Bが見つかるようになります。

おわりに

モジュール単体を実行する時ってどんな時かと言うと、自分の場合は単体テストをしている時でした。
pytestで単体テストを作成中、importエラーで小一時間詰まったので、本記事を書くに至りました。

調査がちょっと雑なので、もしかしたら間違ってる箇所があるかもしれませんが、お気づきの際はご指摘もらえると大変ありがたいです。
(自分で見返してみても日本語が分かりにくい箇所がちょいちょいあるので、随時修正していこうと思ってます。。)

あと、解決策2種類しか書いてませんが、たぶんカレントディレクトリ変える方法とかもあるのではと思ってます。
ただそこは調査できてないので、余力があれば調べて追記したいと思います。

以上です!ご精読ありがとうございました!

112
66
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
112
66

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?