CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: 明示的な相対インポートの使い方

Python のインポートには、次のように3つの種類がある。

  • 暗黙の相対インポート
  • 明示的な相対インポート
  • 絶対インポート

このうち、暗黙の相対インポートについては Python 3 で廃止されたので使ってはいけない。 となると、残るは明示的な相対インポートか絶対インポートのどちらかを使うことになる。 ただ、これまで明示的な相対インポートは使わずに、もっぱら絶対インポートだけを使ってきた。 今回は、これまで明示的な相対インポートを避けていた理由と、その解決策が分かったのでそれについて書いてみる。

使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.11.5
BuildVersion:   15F34
$ python --version
Python 3.5.1

下準備

今回は mypackage というパッケージを用意して、その中で作業する。 Python におけるモジュールとパッケージという概念については、このブログでも記事にしたことがあるのでそちらを参照のこと。

blog.amedama.jp

まずは mypackage パッケージを作る。

$ mkdir mypackage
$ touch mypackage/__init__.py

次に、先ほど作った mypackage の中に mymodb というモジュールを追加する。 このモジュールには greet() という関数が用意されている。

$ cat << 'EOF' > mypackage/mymodb.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-


def greet():
    print('Hello, World!')
EOF

以上で下準備は完了。 今回のポイントは、上記の mymodb モジュールをどのようにインポートして使うべきか、ということ。

絶対インポート

先ほど用意した mymodb モジュールは、同じパッケージに新たに追加する mymoda モジュールから使うことにする。 まずは mymodb モジュールを絶対インポートでインポートするパターンを確認する。

絶対インポートでは、トップレベルのパッケージ名からインポートを記述する。 要するにインポートは from mypackage から始まることになる。

$ cat << 'EOF' > mypackage/mymoda.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from mypackage import mymodb


def main():
    mymodb.greet()


if __name__ == '__main__':
    main()
EOF

絶対インポートを使うときは、次のようにして mymoda モジュールを実行できる。 というか、これまではこうやってきた。 PYTHONPATH は、そのままではパッケージの検索パスが mymoda.py を実行したディレクトリになってしまうので指定している。 これがないと Python インタプリタが mypackage を見つけることができない。

$ PYTHONPATH=. python mypackage/mymoda.py
Hello, World!

ただし、この記事を最後まで読むと上記の実行の仕方は汎用性が低いイマイチなやり方ということがわかる。

明示的な相対インポート

次は、明示的な相対インポートを使うパターン。 明示的な相対インポートでは、その名の通りインポートするモジュールを相対パスのように指定する。 これには、パッケージ名など途中のパスの名前が変わっても変更の手間が少ないというメリットがある。

例えば同じパッケージ以下にあるモジュールであればインポートは from . から始まる。

$ cat << 'EOF' > mypackage/mymoda.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from . import mymodb


def main():
    mymodb.greet()


if __name__ == '__main__':
    main()
EOF

では、これも先ほどの絶対インポートと同じように実行してみよう。

$ PYTHONPATH=. python mypackage/mymoda.py
Traceback (most recent call last):
  File "mypackage/mymoda.py", line 4, in <module>
    from . import modb
ValueError: Attempted relative import in non-package

と、何やら例外になってしまった。

これが、これまで明示的な相対インポートを使うことを避けてきた理由。 開発中に、パッケージ内にある特定のモジュールをどのように実行すれば良いのか分からなかった。

解決策

上記の例外を回避する方法としては python コマンドの -m オプションを使うのが良さげ。 python コマンドの -m オプションを使うと特定のモジュールを指定して実行できる。

つまり、先ほどの例であれば mypackage.mymoda を指定するということ。

$ python -m mypackage.mymoda
Hello, World!

そして、これならパッケージの検索パスがカレントディレクトリになるので PYTHONPATH の指定もいらない。

別の解法としては REPL を起動して呼び出すっていうのもあるかな? でも、これはちょっとめんどくさいね。

$ python
>>> from mypackage import mymoda
>>> mymoda.main()
Hello, World!

ちなみに python コマンドの -m オプションを使うやり方は絶対インポートを使っているときにも通用する。 もう一度 mymoda モジュールを絶対インポートに入れ替えてみよう。

$ cat << 'EOF' > mypackage/mymoda.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from mypackage import mymodb


def main():
    mymodb.greet()


if __name__ == '__main__':
    main()
EOF

先ほどと同様に python コマンドの -m オプションで実行するモジュールを指定する。

$ python -m mypackage.mymoda
Hello, World!

ばっちりだ。

だとすると?

普段から Python で書いたプログラムは python コマンドの -m オプションで実行するクセをつけた方が良いのかも。

例えば次のような単独の Python モジュールがあったとする。

$ cat << 'EOF' > helloworld.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-


def main():
    print('Hello, World!')


if __name__ == '__main__':
    main()
EOF

このときも、もちろん python -m オプションで実行することができる。

$ python -m helloworld
Hello, World!

たぶん、ふつうは Python ファイル (.py) を指定して実行されることが多いんじゃないかな。

$ python helloworld.py
Hello, World!

前者の方が、パッケージまで考えたときに汎用性がありそうだなと。

まとめ

  • Python には3種類のインポートがある

    • 暗黙の相対インポート
    • 絶対インポート
    • 明示的な相対インポート
  • 暗黙の相対インポート

    • Python 3 で使えないので選択してはいけない
  • 絶対インポート

    • トップレベルのパッケージ名からインポートする
    • パッケージ名とか途中の名前が変わると変更に手間がかかる
  • 明示的な相対インポート

    • 今のパッケージから相対的なパスでインポートする
    • パッケージ名とか途中の名前が変わっても変更の手間が少ない
  • 明示的な相対インポートを使うと Python ファイル (.py) を指定して実行できない

    • 代わりに python -m <package.module> を使う
    • むしろ絶対インポートでも上記は使える
    • 普段からこれで実行すると良い気がする