LoginSignup
1
2

More than 3 years have passed since last update.

【Python】エントリポイントの違いによるimport問題の解決

Last updated at Posted at 2020-06-11

アプリケーション本体のエントリポイントから実行する場合は実行上何の問題がなくとも、
各モジュールファイルをエントリポイントとして起動した場合には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を使用したい場合は、

/src/module1.py
# -*- coding: utf-8 -*-

import module2 # ImportError!

ではダメで、

/src/module1.py
# -*- 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()を記述することで解決する方法です。
割と一般的な方法かなと思います。

/src/module1.py
# -*- 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の中身は次のようになります。

/src/__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されるように動作します。
そのため、

  1. __init__.pyのimport時に前述のsys.path.append()が実行され、srcディレクトリがpythonのパスに追加される。
  2. module1.pyがimportされるが、すでにsrcディレクトリがパスに追加されているため、import module2の指定でmodule2.pyをimportできる。

という動作になります。
この方法であれば、srcディレクトリ内にある各モジュールにはsys.path.append()が不要なため、コード量を削減することができます。

ただし、sys.path.append()を使用する以上、名前空間汚染の問題は残ります。

ImportErrorをcatchして分岐する方法

sys.path.append()は使用せず、ImportErrorをcatchしてimport先を動的に変化させる方法です。
具体的には、下記のようなコードになります。

/src/module1.py
# -*- 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して分岐する方法

ただし、どの方法にもデメリットが存在するため環境に合わせて使い分ける必要があるのかなあと思っています。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2