階層構造になってるディレクトリをインポートによっていったりきたりするスクリプトを書いていたら、相対インポートの仕様に咬まれまくって大変だったのでわかったことを書いておく。

以下のような構造のディレクトリで、main.pyからd.pyをインポートし、さらにd.pyからa.pyとb.pyとc.pyを実行したい。その際パッケージインポートを使うので、全ディレクトリには__init__.pyを置いてある。

dir0
  ├─__init__.py
  ├─main.py    : こいつを実行してd.pyをimportして…
  ├─a.py
  └─dir1
      ├─__init__.py
      ├─b.py
      └─dir2
          ├─__init__.py
          ├─c.py
          └─d.py       : こいつからa,b,c.pyをimportしたい。

b.pyとc.pyをインポートするだけなら以下のような感じで可能だ。

dir0
  ├─__init__.py
  ├─main.py    : from dir1.dir2 import d
  ├─a.py
  └─dir1
      ├─__init__.py
      ├─b.py       : print("ここはb.py")
      └─dir2
          ├─__init__.py
          ├─c.py       : print("ここはc.py")
          └─d.py       : from .. import b; from . import c
# main.pyの実行結果
ここはb.py
ここはc.py

この例のd.pyの書き方は、たぶん相対インポートと言っていいのだろう。なおびっくりなことに、d.pyは以下のような書き方でもよい。

          └─d.py       : from dir1 import b; from dir1.dir2 import c

つまりインポートのつながりによってディレクトリが階層構造になった場合、絶対パスのルートはカレントディレクトリになるのだろう。ちなみに、どちらの例でもd.pyは単体で実行するとエラーが出る。これはどうやらカレントディレクトリより上層を参照することはできないのが原因のようだ。

上記のような感じでa.pyをインポートすることはできない。というのも、相対インポートでは、カレントディレクトリの参照およびカレントディレクトリを通過する参照はできないからだ。そこで、相対インポートは諦めて、d.pyでインポートパスにカレントディレクトリを登録し直接インポートを行うという手口でいく。

dir0
  ├─__init__.py
  ├─main.py    : from dir1.dir2 import d
  ├─a.py       : print("ここはa.py")
  └─dir1
      ├─__init__.py
      ├─b.py       : print("ここはb.py")
      └─dir2
          ├─__init__.py
          ├─c.py       : print("ここはc.py")
          └─d.py       : d.pyの内容は以下
# d.pyの内容
import sys, os
sys.path.append(os.getcwd())
import a
from .. import b
from . import c

import文によって行われるモジュールの参照は、sys.pathに入っているパスに基づいている。だもんで os.getcwd() でカレントディレクトリのパスを取得し、sys.pathに登録しちまえばよい。さすればa.pyのインポートに相対インポートは必要ない。というかここまでくれば相対インポートがもはや必要ないな。

# もはや相対インポートを使わないd.py
import sys, os
sys.path.append(os.getcwd())
import a
sys.path.append(os.getcwd() + "\\dir1")
import b
sys.path.append(os.getcwd() + "\\dir1\\dir2")
import c

もうこれでいんじゃね? と思うほどラクだ。相対インポートに from なんちゃら import なんちゃら という記述を使ってしまうと、モジュールの名前空間のまるごとコピーを行う from module import * が使えないのが地味に気になっていたのだが、この手法を使えばその心配も消滅する。 相対インポート調査レポートの締めが相対インポート使わなくてもよくね? なのはアレだが理解が進んだのでよしとしよう。

相対インポートの仕組みに関してはそこそこ知識が集まったけれども、実際にやってみたいのは、そうしてインポートしたファイル内にクラスを作り、その中で相互にいったりきたりするスクリプトなんだよなあ…。白状してしまうとそうやって作りたいのはゲームブックみたいなものなんだけど、いつになったらゲーム自体に取り掛かれるのかすこぶる不安だ。

以下の記事からリンクされています