Python で自作モジュールを扱うときに困ったこと

## はじめに
この記事は [東京理科大学 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/tokyo_university_of_science) 23日目の記事です．
現在東京理科大学工学部情報工学科の3年になる自分が書いていきます．

今回書く内容は， Python でアプリケーションを開発する大学の課題 (なんなら明日プレゼン) に取り組んだときに直面したサブモジュールのお話です．
当初の予定では専攻分野でもあるので，機械学習関連について書きたいと思っていたんですが，少し他のことで忙しくて間に合わなかったので，他で投稿しようとしていた備忘録を投げときます．すいません．

## モジュールの import
Python ではモジュールを呼び出すときには `import ***` や `from *** import ***` と書きますが，自作モジュールを扱う際には少し注意が必要です．

解説のために以下のような構造で考えます．
(以降，サブパッケージのサブモジュールのことをサブモジュールと書きますのでご留意ください．)

```bash
.
├── root.py
└── sub_dir
    ├── sub.py
    └── subsub_dir
        └── subsub.py
```

### メインモジュールからサブモジュールの呼び出し
ここで，`root.py` から `sub.py` と `subsub.py` を呼び出したいとします．
このときは簡単で，

```root.py
from sub_dir import sub
from sub_dir.subsub_dir import subsub
```

とすれば呼び出せることは容易にわかると思います．

### サブモジュールからサブモジュールの呼び出し
では，`sub.py` から　`subsub.py` を呼び出し，それを `root.py` で呼び出したいときはどうすればよいでしょうか．

```root.py
from sub_dir import sub
```

```sub.py
from subsub_dir import subsub
```

と最初は書きたくなりますが，これで `root.py` を実行すると，

```bash
Traceback (most recent call last):
  File "root.py", line 1, in <module>
    from sub_dir.sub import sub_func
  File "/home/skcvim/tmp/root/sub_dir/sub.py", line 1, in <module>
    from subsub_dir import subsub
ModuleNotFoundError: No module named 'subsub_dir'
```

と，モジュールが見つからないというエラーが起きます．
これは，内部的には実行しているディレクトリから相対的に `from subsub_dir import subsub` を参照しようとして起きています．
ですので，

```sub.py
from .subsub_dir import subsub
```

このように，明示的に相対 import を記述してあげると，エラーなく読み込むことができます．

> パッケージ内参照
> https://docs.python.jp/3/tutorial/modules.html#intra-package-references

## ファイルの読み込み
ここから少し話が変わって，以下のようなディレクトリ構造で考えてみます．

```bash
.
├── root.py
└── sub_dir
    ├── sub.py
    └── sub.txt
```

### メインモジュールからテキストファイルの読み込み
まず，`root.py` から `sub.txt` を読み込みたいとします．
このときはモジュール同様に，

```root.py
with open('./sub_dir/sub.txt') as f:
    print(f.read())
```

と，ごく一般的な文法で記述できます．

### サブモジュールからテキストファイルの読み込み
では，`sub.py` から `sub.txt` を読み込み，それを `root.py` で呼び出したいときはどうすればよいでしょうか．

```root.py
from sub_dir import sub
```
```sub.py
with open('./sub.txt') as f:
    print(f.read())
```

と書いてしまうと，モジュール同様に，

```bash
Traceback (most recent call last):
  File "root.py", line 1, in <module>
    from sub_dir import sub
  File "/home/skcvim/tmp/root/sub_dir/sub.py", line 1, in <module>
    with open('./sub.txt') as f:
FileNotFoundError: [Errno 2] No such file or directory: './sub.txt'
```

と，今度はファイルが見つからないというエラーが起きてしまいます．
先ほどと同じように，参照に関するエラーなのですが，今度は少し面倒な解決法になります．

```sub.py
import os

with open(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'sub.txt')) as f:
    print(f.read())
```

これで `root.py` から `sub.txt` を `sub.py` を介して読み込むことができました．
結局，相対的に読み込むのは難しいから絶対パスで参照しているだけなのですが，分解して解説したいと思います．

まず `__file__` の部分ですが，これはカレントディレクトリから実行ファイルの相対パスが返ってきます．
これを `os.path.dirname(__file__)` とすることで，実行ファイルが存在するディレクトリの相対パスが返ってきます．
さらに，`os.path.abspath()` で呼び出すディレクトリが変わったときに対応するために，相対パスを絶対パスに変換しています．
最後に返ってきた絶対パスと，参照したいテキストファイル名を `os.path.join` で結合することで，メインモジュールから呼び出したモジュールを介して任意のディレクトリのテキストファイルを読み込むことができました．

## まとめ
なんでテキストファイル読み込むのにこんなこと書かなきゃいけないのかって感じですが，意外と困ったので記事にしておきました．
何もテキストファイルに限定した話ではなく，`keras.models.load_model()` とか，`PIL.Image.open()` とか，意外と使うシーンがあるのかなと思います．

もっとスマートな方法あったら教えてください．

あと一週間で今年も終わりですし，もう一本くらい記事書けたらいいなって思ってますが，そろそろ参加している Kaggle のコンペが締め切りラッシュなので色々頑張りたいですね．