【簡単Python】サブディレクトリ内の自作モジュールを上手にインポートする方法

アフィリエイト広告を利用しています。
QRコード
【QRコード】PC<-->スマホの切り替えにご利用ください
アイキャッチ画像
Information

2024年10月11日 全面改定

運営者・ポテ

いつもありがとうございます。

「ノンプログラマー向けPython解説シリーズ」へようこそ。

本稿では、「サブディレクトリ内にある自作モジュールを上手にインポートする方法」を解説いたします。


プログラムの規模が大きくなればなるほど、コードを機能ごとに分割し、別モジュールとして管理することが一般的です。

理由は以下の通りです。

  • 再利用性の向上
    同じコードを何度も書く必要がなくなり、効率的にプログラムを開発することができます。
  • デバッグの容易さ
    プログラムを機能ごとに分割することで、各機能を個別にテストすることができるようになるため、デバッグが容易になります。
  • 可読性と保守性の向上
    機能ごとにプログラムを分割することで、分割された各部分が何を行っているのかが理解しやすくなります。また、問題があった場合でも、該当モジュールだけ修正すれば良いため、プログラムの保守性が向上します。

このようにコードを機能ごとに分割し、別モジュールとして管理すると、たくさんの利点があります。

しかし、一方でこれは、分割した各モジュールを"インポート"して使う必要が生じるということでもあります。

一般的に、モジュールはサブディレクトリにまとめておくことが多いです。

つまり、親ディレクトリ内にあるメインファイルとサブディレクトリ内にある各モジュール、あるいはサブディレクトリ内の各モジュール間で、互いに参照する必要が生じるということです。

この参照のパスのつなぎ方の記述を間違えるとエラーが発生します。プログラム作成に利点をもたらすどころか、修復に時間を費やすことになってしまいます。

これは、コードを書く人にとって「あるある」だと思いますが、このようなちょっとしたことの修復作業で1日が過ぎてしまう・・・。そのような経験はないでしょうか。

このようなことがないように、上手なパスのつなぎ方を覚えておくことは非常に重要です。

そこで本稿では、上手なパスのつなぎ方、つまり、エラーを発生させずに自作モジュールをインポートする方法を解説いたします。


ことばの意味

  • モジュール
    特定の機能をもったコードの集合体です。ソフトウェア作成における部品のようなものです。

サブディレクトリ内の自作モジュールを上手にインポートする方法

シナリオ

以下のディレクトリ構成において、「main.pysub1.py」及び「sub1.pysub2.py」の間でモジュールを参照します。

your_program/
├─ main.py
└─ src/
  ├─ __init__.py
  ├─ sub1.py
  └─ sub2.py

なお、__init__.pyは、Python のモジュールを格納するサブディレクトリを「パッケージ」として認識させるためのファイルです。Python 3.3 以降では、_init__.pyがなくてもパッケージとして認識されますが、初期化やモジュールの公開範囲の制御をする場合は、依然として利用されています。今回のプログラムにおいては、無くても問題ありません。

エラーが起こるインポートの仕方

以下にエラーが発生するコードを示します。

コード

import src


# 'sub1' モジュールの関数 'do_sub2' を実行
src.sub1.do_sub2()
# 空
import sub2


def do_sub2():
    sub2.say_hello()


if __name__ == '__main__':
    do_sub2()
def say_hello():
    print('>>> Hello')
    input('>>> Press Enter key to Shutdown')


if __name__== '__main__':
    say_hello()

コード実行結果

main.pyを実行すると以下のエラーが発生します。

AttributeError: module 'src' has no attribute 'sub1'

これは、Pythonが src というパッケージの中に sub1 が見つけられなかったことを意味しています。つまり、Python は、srcをサブモジュールを含むパッケージとして認識していないということです。

main.pyimport src というコードに問題があります。

運営者・ポテ

ふーむ

修正案1(あまり良くない方法)

コード

srcの中からsub1.py を見つけられるようにするために、main.py の1行目を以下のように書き換えてみましょう。

from src import sub1    # ここを変えた


# 'sub1' モジュールの関数 'do_sub2' を実行
sub1.do_sub2()

モジュールをインポートするコードをfrom src import sub1 と書き換えると、Pythonは src というパッケージから直接 sub1 モジュールを探すようになります。この形式により、Pythonは src パッケージ内のモジュール sub1 を認識してインポートできるようになります。つまり、src をサブモジュールが含まれるパッケージとして正しく扱えるようになり、エラーが解消されます。

なお、他のスクリプト(__init__.pysub1.pysub2.py)の修正はありません。

コード実行結果

ModuleNotFoundError: No module named 'sub2'

最初のエラーは解消されましたが、別のエラーが出ました。これは、sub1.pyimort sub2 が失敗しているという意味です。ここです↓

import sub2    # ここが失敗している


def do_sub2():
    sub2.say_hello()


if __name__ == '__main__':
    do_sub2()

この現象について詳しく解説していきます。

実は、このエラーはsub1.py単独で実行するとエラーが出ません。正常に以下の結果が返ってきます。

>>> Hello
>>> Press Enter key to Shutdown

つまり、sub2.py 単独で実行すると成功し、main.pyから実行すると失敗するということです。

原因はimport sub2 という書き方が、同階層内あるモジュールを相対パスでインポートする書き方だからです。

sub1.py 単独で実行する場合、カレントディレクトリは src ディレクトリになり、そのディレクトリ内にあるsub2.pyを相対パスでインポートすることができます。

しかし、main.py から sub1.py を実行する場合、カレントディレクトリが main.pyが置いてあるディレクトリ(src の親ディレクトリ)になってしまいます。このディレクトリには、sub2.pyがありませんので、当然sub2.py が見つからないというエラーが発生するのです。

つまり、カレントディレクトリが変わってしまい、Pythonが正しいパスでモジュールを探せていないために、エラーが発生しているのです。

修正案2(基本的な方法)

main.pyから実行した時だけ正常に動作すれば良いのであれば、sub1.pymain.pyと同様に from src import sub2でインポートする方法があります↓

from src import sub2    # ここをmain.pyと同様に絶対パスでのインポートに変える


def do_sub2():
    sub2.say_hello()


if __name__ == '__main__':
    do_sub2()

この方法ですとmain.pyから実行すればエラーは発生しません。以下のように正常に動作します。

>>> Hello
>>> Press Enter key to Shutdown

しかし、sub1.py単独で実行すると以下のようなエラーが発生します。

ModuleNotFoundError: No module named 'src'

このエラーが発生する理由は、Python のインポートシステムがsrcパッケージを見つけられていないことによります。

プログラムを作成する際は、sub1.py単独で実行したときも、main.pyから実行した時も正常に動作させることが好ましいです。なぜならば、モジュール単体でテストを行うのに便利だからです。

そのため、アプローチを変えてみます。以下に解説していきます。

コード

Python の 検索パスにsrcディレクトリを追加する方法をとります。

# 標準ライブラリのインポート
import sys
import os


# src ディレクトリを Python の sys.path 追加
sys.path.append(
    os.path.join(os.path.dirname(os.path.abspath(__file__)), 'src')
)


# 'src' ディレクトリにある 'sub1' モジュールをインポート
from src import sub1


# 'sub1' モジュールの関数 'do_sub2' を実行
sub1.do_sub2()
運営者・ポテ

解説していきます。

# 標準ライブラリのインポート
import sys
import os

ここでは、Python の標準ライブラリである sysos をインポートしています。これらのファイルパスの操作や、Python の検索パスを扱うために使用します。

# src ディレクトリを Python の sys.path 追加
sys.path.append(
    os.path.join(os.path.dirname(os.path.abspath(__file__)), 'src')
)

ここでは、sys.path.append() で、src ディレクトリを Python の検索パスに追加しています。これにより、src内のモジュールをインポート可能にします。以下に詳細を解説します。

  1. os.path.abspath(__file__)
    • __file__ は、現在実行中のファイルのパスを表します。os.path.abspath(__file__) を使うと、現在のスクリプトの絶対パスが取得できます。
  2. os.path.dirname(...)
    • os.path.dirname() は、ファイルのディレクトリ部分のみのパスを取得する関数です。これにより、現在のスクリプトが置かれているディレクトリを取得することができます。
  3. os.path.join(..., 'src')
    • os.path.join() を使って、現在実行中のスクリプトが置いてあるディレクトリ(os.path.dirname(os.path.abspath(__file__)))に対して src というフォルダ名を追加しています。これにより、src ディレクトリへのパスが生成されます。
  4. sys.path.append(...)
    • sys.path は、Python がモジュールを探す際に使用する検索パスのリストです。
    • sys.path.append() を使って src ディレクトリをこのリストに追加することで、Python が src 内のモジュールをインポートすることが可能になります。
# 'src' ディレクトリにある 'sub1' モジュールをインポート
from src import sub1

ここでは、srcディレクトリ内のsub1モジュールをインポートしています。

# 'sub1' モジュールの関数 'do_sub2' を実行
sub1.do_sub2()

最後のここでは、sub1 モジュール内の do_sub2 関数を呼び出して実行しています。

なお、他のスクリプト(__init__.pysub1.pysub2.py)の修正はありません。

コード実行結果

main.pysub1.py、どちらから実行しても以下の結果が返ってきます。

>>> Hello
>>> Press Enter key to Shutdown

正常に実行することができました。

これでひと通り解決はしましたが、ひとつ注意しないといけないことがあります。スクリプトを .exe 化するケースです。以下に解説します。

注意点

Python で効率化ツールなどを作成しても、普通の会社や研究室では自分以外の人全員に Python がインストールされていることは稀だと思います。つまり、そのコミュニティに配るためには、PythonがインストールされていないPCでも動作するように .exe化 して配る必要が生じます。

Pythonスクリプト(.py)を直接実行する場合と異なり、.exe化した実行ファイルを実行すると、実行ファイルが一時ディレクトリ(Temp)に展開されてしまいます。つまり、Pythonスクリプト(.py)と、.実行ファイル(.exe)では、実行したときのカレントディレクトリが変わってしまうのです。

上述のコードには、このカレントディレクトリの違いの影響を受けるところがあります。ここです↓

os.path.abspath(__file__)

このコードは、以下のように動作します。

  • Pythonスクリプト(main.py)で実行した場合は、このスクリプトが置いてあるディレクトリのパスを取得する。
  • 実行ファイル(main.exe)で実行した場合には、実行ファイルが展開された一時ディレクトリ(temp)のパスを取得する。

つまり、実行ファイル(main.exe)で実行した場合は、以下の箇所においてエラーが発生します。一時ディレクトリ内にsrcディレクトリを見つけることができなくなるためです。

# src ディレクトリを Python の sys.path 追加
sys.path.append(
    os.path.join(os.path.dirname(os.path.abspath(__file__)), 'src')
)

以下のようなエラーが発生します。

実行ファイルで実行した場合のエラー
実行ファイル(.exe)で実行した場合のエラー

この現象の解決方法を次のセクションで解説します。

修正案3(より良い方法)

上述の、.exe化した際の問題も解決する方法を以下に示します。

コード

# 標準ライブラリのインポート
import sys
import os


# 自身が存在するディレクトリのパスを取得する関数
def get_current_directory():

    # PyInstaller でパッケージ化されている場合の処理
    if getattr(sys, 'frozen', False):
        # 実行ファイルのディレクトリパスを取得
        return os.path.dirname(sys.executable)

    else:
        # 通常のスクリプト実行時のディレクトリパスを取得
        return os.path.dirname(os.path.abspath(__file__))


# 実行時の自身が存在するパスを取得
current_directory = get_current_directory()
print(current_directory)


# src ディレクトリを Python の sys.path 追加
sys.path.append(os.path.join(current_directory, 'src'))


# 'src' ディレクトリにある 'sub1' モジュールをインポート
from src import sub1


# 'sub1' モジュールの関数 'do_sub2' を実行
sub1.do_sub2()
運営者・ポテ

解説していきます。

# 標準ライブラリのインポート
import sys
import os

ここでは、Python の標準ライブラリである sysos をインポートしています。これらは、ファイルパスの操作や、Python の検索パスを扱うために使用します。

# 自身が存在するディレクトリのパスを取得する関数
def get_current_directory():

    # PyInstaller でパッケージ化されている場合の処理
    if getattr(sys, 'frozen', False):
        # 実行ファイルのディレクトリパスを取得
        return os.path.dirname(sys.executable)

    else:
        # 通常のスクリプト実行時のディレクトリパスを取得
        return os.path.dirname(os.path.abspath(__file__))
運営者・ポテ

ここがポイント

ここでは、実行されているスクリプトのディレクトリのパスを取得しています。パッケージ化された実行ファイル(.exe)の場合と、Pythonスクリプト(.py)の場合とで、パスの取得方法を変えています。どちらであっても、一時ディレクトリではなく、スクリプトが置いてあるディレクトリを取得しています。

  1. if getattr(sys, 'frozen', False)::
    • getattr関数は、指定したオブジェクトから属性の値を取得するための関数です。第1引数はオブジェクト、第2引数は属性の名前、第3引数は指定した属性が存在しない場合に返すデフォルト値です。第3引数を省略すると、属性が存在しない場合は AttributeError が発生します。
    • PyInstallerなどでパッケージ化されている場合は、sysfrozen属性が追加されます。つまり、この属性がTrueの場合、実行ファイルがパッケージ化されているとみなします。なお、sysは、Pythonの標準ライブラリのひとつで、システム関連の機能を提供するモジュールです。
  2. return os.path.dirname(sys.executable)
    • パッケージ化されている場合、sys.executableから実行ファイルのディレクトリを取得します。つまり、一時ディレクトリではなく、.exeが置いてあるディレクトリを取得します。
  3. return os.path.dirname(os.path.abspath(__file__)):
    • パッケージ化されていない通常のPythonスクリプト(.py)の場合は、そのスクリプトの絶対パスを取得し、そのディレクトリを返します。
# 実行時の自身が存在するパスを取得
current_directory = get_current_directory()
print(current_directory)

ここでは、現在のスクリプトが存在するディレクトリのパスを変数 current_directory に格納し、それをprint関数で表示しています、

# src ディレクトリを Python の sys.path 追加
sys.path.append(os.path.join(current_directory, 'src'))

ここでは、sys.path.append() で、src ディレクトリを Python の検索パスに追加しています。これ以降のコード解説は、前セクションと同じです。

コード実行結果

Python スクリプト(.py)、実行ファイル(.exe)どちらで実行しても以下の結果が返ってきます。

>>> Hello
>>> Press Enter key to Shutdown

正常に実行することができました。

運営者・ポテ

以上で解説は終了です。ありがとうございました。

おわりに

運営者・ポテ

ご覧いただきありがとうございました。

本稿では、「サブディレクトリ内の自作モジュールを、エラーを発生させずにインポートする方法」を解説いたしました。

お問い合わせやご要望等ございましたら、「お問い合わせ/ご要望」またはコメントにて、ご連絡いただければ幸いでございます。

皆様の人生がより一層素晴らしいものになるよう、少しでもお役に立てれば幸いでございます。

尚、当サイトでは様々な情報を発信しております。もしよろしければ、トップページもご覧いただけると幸いでございます。

>>トップページ


記事関連経験

  • Python 3 エンジニア認定基礎試験
    経済産業省が定めたガイドライン「ITスキル標準(ITSS)」に掲載されている民間資格です。
  • Python 使用経験 約5年
    実務に使用する アプリを多数作成してきました。

Python プログラミングスキルアップのための参考情報

ここでは参考図書を紹介いたしますが、これに限らず自分に合うものを選ぶことが重要だと考えております。皆様の、より一層のご成功を心からお祈りしております。


「独学プログラマー」というPythonを題材にした書籍は大変勉強になりました。Pythonの技術解説だけにとどまらず、プログラミングの魅力や基本的な知識、思考法、仕事の進め方まで幅広く学べます。


こちらの記事でも紹介しております。もしよろしければご覧ください。


また、Pythonに関する書籍は多数出版されています。興味のある方は、チェックしてみてください。

\チェックしてみよう/

\チェックしてみよう/

\チェックしてみよう/

QRコード
【QRコード】PC<-->スマホの切り替えにご利用ください

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です