アプリケーション本体のエントリポイントから実行する場合は実行上何の問題がなくとも、
各モジュールファイルをエントリポイントとして起動した場合にはimport文で躓くということがあったので、その解決法を調査しました。
- 6/12: try~except文のexceptがcatchになっていたため修正しました。
importはエントリポイントとしたディレクトリが起点
サンプルとして、下記のようなディレクトリ・ファイル構成を考えます。
main.py ... アプリケーションとしての実行ファイル
|
|--[src] ... モジュールを格納するディレクトリ
|
|-- module1.py ... main.pyから呼び出すモジュール
|-- module2.py ... module1.pyから呼び出すモジュール
アプリケーションとしてのエントリポイントがmain.pyの場合、pythonのパスとして認識されるのはmain.pyのあるディレクトリのみです。
そのため、このアプリケーションを構成する全てのファイルは、main.pyのあるディレクトリを起点としたimportが必要になります。
つまり、上記のディレクトリ構造でmodule1.pyからmodule2.pyを使用したい場合は、
# -*- coding: utf-8 -*-
import module2 # ImportError!
ではダメで、
# -*- coding: utf-8 -*-
import src.module2 # OK
としなければなりません。
srcディレクトリ内部のファイルから起動できなくなる問題
運用時はこの記述でよいのですが、module1.py内にテストコードを書くような場合に問題が発生します。
ここでいうテストコードというのは、module1.py内のクラスや関数などの動作を確認するものを想定しています。
書いたコードの動作をその場で確認するために、そのファイル内にある関数を呼び出す簡単なコードを書いてF5キーを押すのは割とよくある光景なのではないでしょうか?
しかし、import src.module2
という記述をしてしまうと、module1.pyをエントリポイントとしてプログラムを起動した場合にsrc
の部分が認識できずImportErrorが発生します。
これは、module1.pyを起点とした場合の実行ディレクトリがsrcディレクトリに変わってしまうためです。
srcディレクトリ内には、src
というディレクトリは存在しないのです。
本記事ではこの問題を解決し、main.pyからでもmodule1.pyからでもプログラムを起動できるようなアイデアを3つ紹介します。
ちなみに、どれもデメリットがありこれをやればOKというものではないです。
もっとスマートな方法があれば募集しておりますのでコメントください。
- モジュールのファイル内でsys.path.append()する方法
- __init__.pyでsys.path.append()する方法
- ImportErrorをcatchして分岐する方法
モジュールのファイル内でsys.path.append()する方法
module1.py内にsys.path.append()
を記述することで解決する方法です。
割と一般的な方法かなと思います。
# -*- coding: utf-8 -*-
import sys, os
sys.path.append(os.path.join(os.path.dirname(__file__), '.'))
import module2
これは、pythonのパスにmodule1.pyのファイルのあるディレクトリを追加するというものです。
これを実行することで、pythonのパスにsrcディレクトリが追加されることになり、import文にsrc
がなくてもエラーが発生しなくなります。
つまり、module2.pyのimportは、import module2
のみでよいことになります。
しかしこの方法では、モジュールファイルが大量にあった場合にはファイルごとへの上記の記述が必要になってしまう問題があります。
また、sys.path.append()
はmodule1.pyを使用している全ファイルに反映されるため、名前空間が汚染されます。
(main.py内でもimport module2
ができるようになってしまいます。)
__init__.pyでsys.path.append()する方法
srcディレクトリの直下に__init__.pyを配置し、その中でsys.path.append()
を行う方法です。
この場合、ディレクトリ構成は次のようになります。
main.py ... アプリケーションとしての実行ファイル
|
|--[src] ... モジュールを格納するディレクトリ
|
|-- __init__.py ... sys.path.append()を実行するファイル
|-- module1.py ... main.pyから呼び出すモジュール
|-- module2.py ... module1.pyから呼び出すモジュール
また、__init__.pyの中身は次のようになります。
# -*- coding: utf-8 -*-
import sys, os
sys.path.append(os.path.join(os.path.dirname(__file__), '.'))
これは何をしているのかというと、main.pyでimport src.module1
をする際には__init__.pyも暗黙的にimportされることを利用しています。
具体的には、main.pyからimport src.module1
を実行する時、まず__init__.pyが先にimportされ、次にmodule1.pyがimportされるように動作します。
そのため、
- __init__.pyのimport時に前述の
sys.path.append()
が実行され、srcディレクトリがpythonのパスに追加される。 - module1.pyがimportされるが、すでにsrcディレクトリがパスに追加されているため、
import module2
の指定でmodule2.pyをimportできる。
という動作になります。
この方法であれば、srcディレクトリ内にある各モジュールにはsys.path.append()
が不要なため、コード量を削減することができます。
ただし、sys.path.append()
を使用する以上、名前空間汚染の問題は残ります。
ImportErrorをcatchして分岐する方法
sys.path.append()
は使用せず、ImportErrorをcatchしてimport先を動的に変化させる方法です。
具体的には、下記のようなコードになります。
# -*- coding: utf-8 -*-
try:
import src.module2 # main.pyから起動した場合はこちらが実行される
except ImportError:
import module2 # module1.pyから起動した場合はこちらが実行される
import src.module2
に失敗したら、代わりにimport module2
を実行するという動作になっています。
通常、アプリケーションはmain.pyから実行されるため、実行ディレクトリからはsrcディレクトリが見えています。
そのため、main.pyからの起動時にはtryブロックの中身は問題なく実行されます。
一方、module1.pyをエントリポイントとして起動した場合はsrcディレクトリが見えないため、ImportErrorが発生します。
そこで、exceptブロックでImportErrorをcatchし、import先の記述を変えることでうまくimportができるようにします。
この手法はsys.path.append()
を使用しないため名前空間の汚染が発生しません。
しかし、各モジュールファイルに上記の記述を追加しなければならないという問題は残ります。
まとめ
本記事では、main.pyからでもmodule1.pyからでもプログラムを起動できるようになる方法を3つ説明しました。
- モジュールのファイル内でsys.path.append()する方法
- __init__.pyでsys.path.append()する方法
- ImportErrorをcatchして分岐する方法
ただし、どの方法にもデメリットが存在するため環境に合わせて使い分ける必要があるのかなあと思っています。