Pythonの相対インポートの注意点

Python

Pythonを勉強してだんだんと規模が大きなものを作るようになってくると、自分が書いた何らかのモジュール(特にパッケージとしてディレクトリの中に入っているもの)をインポートする機会が増えてくると思います。

今回は特に「相対インポート」の注意点について書きます。

インポートの仕方色々

Pythonを学び始めて「外部ファイルのコードを使うときにはインポートする」という知識で十分だろうと、あまりインポートの仕方について深く考えて来なかったのですが、最近インポートが意図せずエラーになることがあって改めて調べ直しました。

外部のファイルに書かれたモジュールを使うためには当たり前のようにimportを使うことになると思いますが、この書き方が色々とあります。

  • 単にimport
  • fromを使ったimport
    • 絶対インポート
    • 相対インポート

pipでインストールしたモジュールは単にimportだけを書くことが多いと思います。

fromを使ったインポートの書き方には「絶対」と「相対」というインポート方法があることは知っていて、「ドット(.)を使って、呼び出し元のファイルからみた相対パスでインポートできる」と理解していました。しかしこの理解が間違っていて、意図しないインポートエラーに遭遇してしまいました。

相対インポートの注意点

単に相対パスでインポートするものではない

「相対インポート」という字面をみて「あぁ、相対パスでインポートする方法ね」という理解をしていたことが間違いでした。

まず、相対インポートはメインモジュール(pythonコマンドで実行するモジュール)では使えないということです。

このことはPythonの公式ドキュメントに記載がありました。以下はv3.9のドキュメントです。

6. モジュール — Python 3.9.18 ドキュメント

相対 import は現在のモジュール名をベースにすることに注意してください。メインモジュールの名前は常に "__main__" なので、Python アプリケーションのメインモジュールとして利用されることを意図しているモジュールでは絶対 import を利用するべきです。

https://docs.python.org/ja/3.9/tutorial/modules.html#intra-package-references

また、teratailの以下の質問の回答も参考になりました。

pythonの相対パスや相対importを呼び出されて使われるファイルに書くことは可読性の観点から非推奨なのでしょうか
例として以下のようなディレクトリ構造があるとして、main.pyはlib.pyをインポートして実行する場合、 lib.pyで相対importや相対パスでファイルを入出力すると、main.pyから見た

「相対import」は、「相対パスで他のモジュールをインポートするもの」ではありません。

(中略)

「相対import」というのは、「同パッケージにある他のモジュールをインポートするもの」です。

pythonコマンドで実行されるメインモジュールに相対インポートの記載があるとエラーになってしまいます。

ImportError: attempted relative import with no known parent package

相対インポートの使い所としては、パッケージの中で他のモジュールを呼び出すときとのことです。

上の階層の相対インポート(..)もパッケージ内のみで有効

相対インポートはドットひとつ(.)で同階層を示し、ドットふたつ(..)だと上の階層を表します。

さて、このドットふたつの場合はどのように動作するのか。

パッケージを超えて相対インポート

以下のような構成で実験してみます。

.
├── main.py
├── mod_a.py
└── pac_b
    └── mod_b.py

main.pyをメインモジュールとして、python main.pyで実行します。

mainからmod_bの中の関数を呼びし、さらにmod_bからmod_a(メインと同階層)を呼び出すために上の階層の相対インポートを試します。

# main.py
from pac_b import mod_b

mod_b.good_bye()
# mod_b.py
from .. import mod_a

def good_bye():
    print("Good Bye")
    mod_a.hello()
# mod_a.py
def hello():
    print("Hello")

こちらは以下のエラーとなります。

ImportError: attempted relative import beyond top-level package

パッケージの中で相対インポート

では、pac_bの中にさらにpac_cディレクトリを作成して、pac_cの中のmod_c.pyから上の階層のmod_bをドットふたつで相対インポートしてみます。

.
├── main.py
├── mod_a.py
└── pac_b
    ├── mod_b.py
    └── pac_c
        └── mod_c.py
# main.py
from pac_b.pac_c import mod_c

mod_c.hi()
# mod_c.py
from .. import mod_b

def hi():
    print("Hi!")
    mod_b.good_bye()
# mod_b.py
def good_bye():
    print("Good Bye")

結果は以下のように正常終了しました。

Hi!
Good Bye

まとめ

相対インポートは単純に相対パスでインポートする機能じゃないから注意しないといけないことを学びました。

相対インポートの使い所としては、「パッケージの中で他のモジュールを呼び出すとき」ですね。

コメント

タイトルとURLをコピーしました