Pythonの自作モジュールにてパス取得にハマってしまったのでメモがてらここに
環境
MacOS 10.14.6
プロセッサ 2.4 GHz Intel Core i5
RAM 8 GB 2133 MHz
ゴール
以下のようなディレクトリ構成において、実行しているファイル(ここでは、audrey.pyとする)から、srcディレクトリ内のモジュールおよびdataディレクトリ内のファイルをimportする。
下記のように、dataディレクトリからもsrcディレクトリからも、データをいつでも引っ張れるようにするのが目標。
~/
|
├─ notebook
| └── audrey.py
|
├─ data
| └── raw
| └── wakabayashi.csv
|
└──scr
├── __init__.py
└── kasuga
├── __init__.py
├── onigawara.py
└── apaaaaaaa.py
メインプログラムのaudrey.pyの中はざっくりとこんな感じ。
from src import *
# here, 'path_raw' is the relative path for loading the raw data
raws = pd.load_csv(path_raw + "wakabayashi.csv")
この相対パスは、srcディレクトリ内の__init__.pyでこんな感じに定義している。
srcディレクトリのインポートに成功し、__init__.pyが実行されるとSuccess to import src!tofu
という出力が得られるようになっている。
print("Success to import src!")
path_raw = '../data/row/'
from src.kasuga import onigawara, apaaaaaaa
そして、kasugaディレクトリ内の__init__.pyの中身はこんな感じ。
これもまた、ちゃんとインポートできているかどうかの確認用のprint文。
print("Success to import kasuga!")
行けそうで行けない例
これで単純に終わるのでは?とやってみたものの失敗した例。
from ..src import *
# >>> ValueError: attempted relative import beyond top-level package
このエラーメッセージを読むと、どうやらPythonでは現在の実行しているファイル(ここではaudrey.py)をrootとみなすようで、それより上の階層に対しては参照できないようだ。
解決策(相対パス、絶対パス)
ここでは、sys.pathに親のパスを相対パス、絶対パスの2種類で追加するようにトライしたものをメモする。
相対パスを使用した場合
sys.pathにsrcディレクトリを登録
import sys
sys.path.append('../src/')
from src import kasuga
# >>> ModuleNotFoundError: No module named 'src'
import kasuga
Success to import kasuga!
このやり方だと、srcディレクトリ内部の/kasuga/__init__.pyは実行されるものの、肝心の/src/__init.pyは実行されない。
sys.pathの中身はというと、そのまま'../src/'が入っているようだ。
print(sys.path)
# >>> ['~/notebook', '../src/']
sys.pathに親ディレクトリ(ここでは~/に該当)を登録
今度は、notebookディレクトリ、srcディレクトリの親ディレクトリに該当する部分を相対パスで登録してやろうという。
import sys
sys.path.append('../../~/')
from src import kasuga
# >>> Success to import src!
# >>> Success to import kasuga!
print(sys.path)
# >>> ['~/notebook', '../../~/']
このやり方だと、/src/__init__.pyと/src/kasuga/init.pyのどちらも問題なく実行された。 sys.pathはというと、先ほどの例と同じように、文字列'../../~/'`がそのまま入っている。
ただ、個人的には可読性が低くあまり好きでは無いかなー。
絶対パスを使用した場合
絶対パスを使用しても、もちろん可能である。
あまり、個人的には絶対パスを使用する頻度は高くないなぁ、というのが所感ではある。
ただ、ディレクトリをバシッと指定するため、非常にわかりやすい。
絶対パスの取得には、pathlibが使い勝手が良い(Python 3.4以降)。
以下のコマンドでpathlib.Pathメソッドをimport。
import sys
from pathlib import Path
# absolute current directory will append into 'sys.path'
sys.path.append(str(Path('__file__').resolve().parent))
# >>> /Users/~~~/~/notebook/
# >>> absolute current PARENT-directory will append into 'sys.path'
sys.path.append(str(Path('__file__').resolve().parent.parent))
# >>> /Users/~~~/~/
上のように、Path('current script name').resolve().parentで現在実行しているスクリプトのディレクトリを得ることができる。
そして、Path('current script name').resolve().parent.parentのように.parentを2回重ねることで、その親ディレクトリの絶対パスを取得可能。
ちなみに、ここで__file__は、カレントディレクトリからの実行スクリプトまでの相対パスを取得(スクリプト実行直後であれば、実行スクリプト名;e.g. audrey.py が入る)するものであるため、スクリプト名が変更されようとこのコードは問題なく使用することができる。
絶対パスを取得した後であれば、以下のようにしてsrcディレクトリ内のモジュールをimport可能。
sys.path.append(str(Path('__file__').resolve().parent.parent))
# >>> /Users/~~~/~/
from src import *
# >>> Success to import src!
# >>> Success to import kasuga!
ちなみに、/src/__init__.py内部で/data/raw/への相対パスをpath_rawという変数に格納しているが、それはsrcをimportした後であればaudrey.py上から呼び出すことが可能。
import sys
from pathlib import Path
sys.path.append(str(Path('__file__').resolve().parent.parent))
from src import *
import pandas as pd
tukkomi_god = pd.read_csv(path_raw + 'wakabayashi.csv')
print(tukkomi_god.radio)
# >>> Success to import src!
# >>> Success to import kasuga!
# >>> http://www.allnightnippon.com/kw/
終わりに
絶望的に私は面倒臭がりなんで、なんでも楽できるようにしたい。それで、こんなコードを書きました。
ディレクトリの相対位置関係が変化しない限り使い続けられるし、そのパス取得には実行スクリプトの外の__init__.pyを使用しているのでそれをimportする限りはプログラムの仕様を変える必要もない、わざわざ毎回定義する必要がない。楽。
もっとこうすればさらに楽になるよ、など情報を頂ければ嬉しいです。
そしてこれが貯まりに貯まったストックの中からのQiita初投稿。
ここが読みにくいよ、などあれば教えていただければ幸いにございます。
いつもQiitaの皆さんの投稿に助けられているので、この記事が誰かの助けにならんことを。
バーイ。