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



いつもありがとうございます!
「ノンプログラマー向けPython解説シリーズ」へようこそ。
本稿では、「サブディレクトリ内にある自作モジュールを上手にインポートする方法」を解説します。
プログラムの規模が大きくなればなるほど、コードを機能ごとに分割し、別モジュールとして管理することが一般的です。
理由は以下の通りです。
- 再利用性の向上
同じコードを何度も書く必要がなくなり、効率的にプログラムを開発することができます。 - デバッグの容易さ
プログラムを機能ごとに分割することで、各機能を個別にテストすることができるようになるため、デバッグが容易になります。 - 可読性と保守性の向上
機能ごとにプログラムを分割することで、分割された各部分が何を行っているのかが理解しやすくなります。また、問題があった場合でも、該当モジュールだけ修正すれば良いため、プログラムの保守性が向上します。
このようにコードを機能ごとに分割し、別モジュールとして管理すると、たくさんの利点があります。
しかし、一方でこれは、分割した各モジュールを"インポート"して使う必要が生じるということでもあります。
一般的に、モジュールはサブディレクトリにまとめておくことが多いです。
つまり、親ディレクトリ内にあるメインファイルとサブディレクトリ内にある各モジュール、あるいはサブディレクトリ内の各モジュール間で、互いに参照する必要が生じるということです。
この参照のパスのつなぎ方の記述を間違えるとエラーが発生します。プログラム作成に利点をもたらすどころか、修復に時間を費やすことになってしまいます。
これは、コードを書く人にとって「あるある」だと思いますが、このようなちょっとしたことの修復作業で1日が過ぎてしまう・・・。そのような経験はないでしょうか。
このようなことがないように、上手なパスのつなぎ方を覚えておくことは非常に重要です。
そこで本稿では、上手なパスのつなぎ方、つまり、エラーを発生させずに自作モジュールをインポートする方法を解説いたします。
サブディレクトリ内の自作モジュールを上手にインポートする方法

シナリオ
以下のディレクトリ構成において、「main.py と sub1.py」及び「sub1.py と sub2.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.pyのimport 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__.py、sub1.py、sub2.py)の修正はありません。
コード実行結果
ModuleNotFoundError: No module named 'sub2'最初のエラーは解消されましたが、別のエラーが出ました。これは、sub1.pyの imort 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.pyもmain.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 の標準ライブラリである sys と os をインポートしています。これらのファイルパスの操作や、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内のモジュールをインポート可能にします。以下に詳細を解説します。
os.path.abspath(__file__)__file__は、現在実行中のファイルのパスを表します。os.path.abspath(__file__)を使うと、現在のスクリプトの絶対パスが取得できます。
os.path.dirname(...)os.path.dirname()は、ファイルのディレクトリ部分のみのパスを取得する関数です。これにより、現在のスクリプトが置かれているディレクトリを取得することができます。
os.path.join(..., 'src')os.path.join()を使って、現在実行中のスクリプトが置いてあるディレクトリ(os.path.dirname(os.path.abspath(__file__)))に対してsrcというフォルダ名を追加しています。これにより、srcディレクトリへのパスが生成されます。
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__.py、sub1.py、sub2.py)の修正はありません。
コード実行結果
main.py、sub1.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')
)以下のようなエラーが発生します。

この現象の解決方法を次のセクションで解説します。
修正案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 の標準ライブラリである sys と os をインポートしています。これらは、ファイルパスの操作や、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)の場合とで、パスの取得方法を変えています。どちらであっても、一時ディレクトリではなく、スクリプトが置いてあるディレクトリを取得しています。
if getattr(sys, 'frozen', False)::getattr関数は、指定したオブジェクトから属性の値を取得するための関数です。第1引数はオブジェクト、第2引数は属性の名前、第3引数は指定した属性が存在しない場合に返すデフォルト値です。第3引数を省略すると、属性が存在しない場合はAttributeErrorが発生します。- PyInstallerなどでパッケージ化されている場合は、
sysにfrozen属性が追加されます。つまり、この属性がTrueの場合、実行ファイルがパッケージ化されているとみなします。なお、sysは、Pythonの標準ライブラリのひとつで、システム関連の機能を提供するモジュールです。
return os.path.dirname(sys.executable)- パッケージ化されている場合、
sys.executableから実行ファイルのディレクトリを取得します。つまり、一時ディレクトリではなく、.exeが置いてあるディレクトリを取得します。
- パッケージ化されている場合、
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 プログラミングスキルアップのための参考情報
ここでは参考図書を紹介いたしますが、これに限らず自分に合うものを選ぶことが重要だと考えております。皆様の、より一層のご成功を心からお祈りしております。
「独学プログラマー」というPythonを題材にした書籍は大変勉強になりました。Pythonの技術解説だけにとどまらず、プログラミングの魅力や基本的な知識、思考法、仕事の進め方まで幅広く学べます。
こちらの記事でも紹介しております。もしよろしければご覧ください。
また、Pythonに関する書籍は多数出版されています。興味のある方は、チェックしてみてください。
\チェックしてみよう/
\チェックしてみよう/
\チェックしてみよう/

おわりに


ご覧いただきありがとうございました!
本稿では、「サブディレクトリ内の自作モジュールを、エラーを発生させずにインポートする方法」を解説いたしました。
お問い合わせやご要望等ございましたら、「お問い合わせ/ご要望」フォームまたはコメント欄よりお知らせください。
この記事が皆様のお役に立てれば幸いです。
なお、当サイトでは様々な情報を発信しております。よろしければトップページもあわせてご覧ください。
この記事を書いた人
- ソフトデザイン工房|日々の業務にちょうどいい自動化を
- ■人生を追求する凡人 ■日本一安全で、気の向くままに自分の時間を過ごせる、こだわりのキャンプ場を作るのが夢 ■ソフトデザイン工房運営(個人事業者) - 業務改善アプリケーションをご提供 ■人生は時間そのもの。ひとりでも多くの人が「より良い人生にするために時間を使って欲しい」と願い、仕事のスキルの向上、余暇の充実、資産形成にチャレンジ。





