sec05 - Appendix 1:Pythonのimportを完全理解|絶対インポートと相対インポートの違い・エラー対策まで解説
スポンサーリンク

Pythonのimportを基礎から理解する

これまで学んできたように、Pythonではimport文を使って他のモジュールを読み込むことができます。目的のモジュールまでの階層を、ルートフォルダから辿って指定する方法を「絶対インポート」といいます。

しかし、プロジェクトが大きくなってフォルダ階層が深くなると、ルートフォルダから階層を辿るのが煩雑になってくる場合があります。その時に使えるテクニックが「相対インポート」です。

絶対インポートと相対インポートの使い分け

身も蓋もない前置きになりますが、基本的には「絶対インポート」を使うことをおすすめします。理由は簡単で、コードを読む人が「どのパッケージから読み込んでいるのか」を一目で理解できるからです。

ただ、状況によっては「相対インポート」も理解しておく必要がありますので、ここから説明します。相対インポートは、同じパッケージ(同一パッケージ)内での限定的な利用にとどめるのが推奨されます。

つまり、次のように考えると分かりやすいです。

  • プロジェクト全体の構造をまたぐ →絶対インポート
  • 同じフォルダの中だけで使いたい →相対インポート
スポンサーリンク

Pythonのフォルダ構成(サンプル)

次のような簡単なフォルダ構成を例にして考えてみましょう。

myproject/
├─ main.py
└─ package/
   ├─ __init__.py
   ├─ module_a.py
   └─ module_b.py

このように、プロジェクトのルートがmyprojectで、その中にpackageというフォルダ(=パッケージ)があり、その中にmodule_a.pymodule_b.pyが入っている構成です。

絶対インポートの仕組みと使い方

まず、「絶対インポート」から見てみましょう。絶対インポートとは、「プロジェクトのルートからのパスで指定する」方法です。つまり、プロジェクトのルートを基準とするため、どこから実行しても常に同じように参照できるようにする書き方です。

まず、module_a.pyの中身を次のようにしておきます。

# module_a.py
def hello():
    print('Hello from module_a!')

例えば、main.pyからmodule_aを使いたい場合は、次のように書きます。なお、相対インポートがfrom - import -の書式のみサポートされているため、ここでは書き方を揃えています。

# main.py
from package import module_a

module_a.hello()

この状態で実行すると、次のように表示されます。

Hello from module_a!

このように、「ルート(myproject/)を基準に完全な経路で指定する方法」が絶対インポートです。

相対インポートの書き方と動作解説

次に「相対インポート」です。こちらは、「現在のモジュールの位置を基準にして指定する」方法です。Pythonでは、ドット...を使って階層を表現します。.は「同じ階層」を表し、..は「1つ上の階層」を表します。

例えば、module_b.pyの中から、同じフォルダ内にあるmodule_aを使いたい場合は、次のように書けます。

# module_b.py
from . import module_a

def call_hello():
    module_a.hello()

呼び出しは、main.pyから行います。

# main.py
from package import module_b

module_b.call_hello()

結果は次の通りです。

Hello from module_a!

このコードの流れを整理します。

  • module_bimportすると、module_bは同じ階層(from .)のmodule_aimportを行います。
  • main.pyからmodule_b.call_hello()を実行します。
    • module_b.call_hello()関数内では、module_a.hello()が実行されます。
  • 最終的に、print('Hello from module_a!')が実行され、処理が終了します。

このように、from . import module_aと記述することで、「同じ階層にあるmodule_aimport」することができます。

相対インポートでよく出るエラーの原因と対処法

相対インポートを使うとき、初学者がよくつまずくのが次のようなエラーです。

VisualStudioCodeでmodule_b.pyを開き、直接実行してみましょう。

ImportError: attempted relative import with no known parent package

このエラーメッセージは、「このモジュールがどのパッケージに属しているかを示す親パッケージが不明である」と言っています。直接実行されたファイルは、Pythonの内部で__main__という名前に変わってしまうため、「このモジュールがどのパッケージに属しているのか」が分からなくなります。

ではどうすればよいでしょうか?対策は2つあります。

  • 方法①:ルートから実行する
    • Windowsのコマンドラインから実行する場合
      • ルートディレクトリ(例:myproject/)に移動し、python -mオプションを使ってパッケージとして実行します。
    • VisualStudioCodeでも、launch.jsonに設定を追加することで、同じように実行できます。
  • 方法②:絶対インポートを使う
    • sys.pathmyproject/のパスを追加し、相対インポートではなく絶対インポート(例:from package import module_a)に切り替える方法です。

相対インポートの解決策①:ルートからの実行

コマンドプロンプトで実行する場合

Windowsのコマンドプロンプトで、次の操作を行います。

cdコマンドで、ルートディレクトリ(myproject/)までのパスを「カレントディレクトリ」にします。

> cd C:\Users\xxx\Documents\python101\myproject

pythonコマンドから、-mオプションを付けてpackage.module_bを実行します。実行するモジュールまでの階層は、絶対インポートと同じように指定します。

> python -m package.module_b

コマンドプロンプトの画面は次のように表示されます。

VisualStudioCodeからコマンドプロンプトと同じように実行する

VisualStudioCodeからコマンドプロンプトと同じように実行するには、launch.jsonに実行設定を1つ加えます。

(1) "Run and Debug"の画面に切り替え、歯車アイコンをクリックします。

(2) 画面右下の"AddConfiguration…"ボタンをクリックします。

(3) 表示されるメニューから、"Python Debugger" > "Module" を押下します。

(4) 実行するModule(package.module_b)を指定し、Enterを押下します。

(5) 新しく追加された設定項目に、myproject/までパスを次のように加えます。

"cwd": "C:/Users/xxx/Documents/python101/myproject"

(6) 画面をmodule_b.pyに切り替え、Pythonの実行を"Python Debugger: Module"に切り替えて実行します。

(7) 次のようにコンソールにメッセージが表示されれば成功です。

Hello from module_a!

相対インポートの解決策②:絶対インポートに切り替える

module_b.pyを次のように書き換えます。

# module_b.py
if __name__ == '__main__':
    # 相対インポートのエラー対策として、sys.pathを一時的に追加する
    import sys
    sys.path.append('C:/Users/xxxx/Documents/python101/myproject')  # 皆さんの環境に合わせて書き換えてください

from package import module_a

def call_hello():
    module_a.hello()


if __name__ == '__main__':
    call_hello()
  • 【2-5行目】module_b.pyが直接実行された時に、ルートディレクトリをsys.pathに追加します。
  • 【7行目】絶対インポートに書き換え、ルートからの階層を指定します。

上記のように書き換えることで、Pythonはルートディレクトリを把握することができ、正常にimportできます。

まとめ:絶対・相対インポートの使い分け

今回のポイントを整理しておきましょう。

  • 絶対インポート:プロジェクトのルートを基準にした指定方法。
  • 相対インポート:現在のモジュール位置を基準にした指定方法。(...を使用。)
  • 相対インポートでエラーが出る場合は、「どこから実行しているか」を確認しよう。
  • 基本的には絶対インポートを使うのが安全。

importlibを使った動的インポート(発展編)

少し応用的な話として、Pythonにはimportlibという標準ライブラリがあります。これを使うと、文字列で指定したモジュールを動的に読み込むことができます。

main.pyに次のコードを記述し、実行してみましょう。(Pythonの実行環境は、通常の"Python Debugger: Current File"に戻し、実行します。)

# main.py
import importlib

module_name = 'package.module_a'  # 文字列で指定する
module_a = importlib.import_module(module_name)
module_a.hello()

importlib.import_module()関数を使えば、実行時にどのモジュールを読み込むかを切り替えることができます。

language_manager.pyをimportlibで拡張する

fortune_cookieプロジェクトのlanguage_manager.pyを次のように書き換えます。(Docstringの部分は非表示にしています。)

# manager/language_manager.py

import importlib
import sys


def get_language_module(lang_code):
    try:
        # lang_code に応じて 'message.language_xx' を動的に import する。
        module_name = f'message.language_{lang_code}'
        module = importlib.import_module(module_name)
        return module
    except ImportError as e:
        print(f'ImportError: 指定された言語コード "{lang_code}" に対応するモジュールがありません。')
        print('詳しい情報:', e)
        print('プログラムを終了します')
        sys.exit(1)  # 非ゼロで終了(エラーを示す)

このように記述することで、新しい言語(frcnなど)が追加されたとしても、language_manager.pyのコードを修正する必要がなくなります。

スポンサーリンク