書き出すファイルの名前に､書き出しを実行したPythonファイルの名前を自動で挿入する方法【自作モジュール編】

Pythonでファイルを書き出すときに､保存するファイルに簡単に名前をつけるコードを書いたので記事にします｡
具体的には､**実行しているPythonのファイル名をそのまま保存するファイルの名前に流用**します｡
**ただし､書き出す関数は自作のモジュールからimportしているものとします(前回との差分)｡**

今回の主題となるコードは記事の末尾に記載しています(コピペ用)｡


# 本記事の想定読者
- Pythonユーザー
- 複数のスクリプトを使って図などを書き出すことがある
- 保存したファイルの整理に苦労することがある
- **自作のモジュールをimportして利用することがある(前回との差分)**

# 前回の記事
前回はファイルの書き出し処理が1つのスクリプトの中で完結するケースについて書きました｡
今回は実行するスクリプトとは別に､自作のモジュールを利用するケースについて書きます｡

https://qiita.com/mochi_gu_ma/items/5a91d757563dc58e426a

# 結論
- importしたモジュールの関数を使う場合は`os.path.abspath(sys.modules['__main__'].__file__)`を使う [^1]
[^1]:参照: [How to get filename of the __main__ module in Python?](https://stackoverflow.com/questions/606561/how-to-get-filename-of-the-main-module-in-python)


# 最終的なアウトプットの例: 動物の研究

- 複数の動物について､`matplotlib`で図を作った場合のディレクトリ構成図
- 共通する処理を自作のモジュール`report.py`にまとめている
- `report.py`が自動的にフォルダを作成し､ファイル名に各動物の名前を挿入している
- 動物ごとのスクリプトに**実行ファイル名(動物の名前)についてのコードを書く必要がない**


```:animal_researchのディレクトリ構成図
animal_research/
├── Lion
│   ├── Lion_height_chart.png
│   ├── Lion_lifespan_chart.png
│   └── Lion_weight_chart.png
├── cheetah
│   ├── cheetah_height_chart.png
│   ├── cheetah_lifespan_chart.png
│   └── cheetah_weight_chart.png
├── cheetah.py
├── leptailurus serval
│   ├── leptailurus_serval_height_chart.png
│   ├── leptailurus_serval_lifespan_chart.png
│   └── leptailurus_serval_weight_chart.png
├── leptailurus serval.py
├── lion.py
└── report.py
```


```Python:lion.py
# 他の動物のファイルも全く同じ中身
# 各動物のファイルに動物名(ファイル名)を直接書く必要がないので流用が楽
import report

report.make_figure('weight_chart')
report.make_figure('height_chart')
report.make_figure('lifespan_chart')
```

# 記事の背景
複雑な処理を記述しようとすると､コードが長くなり､読みづらくなりがちです｡
特に`matplotlib`を使う場合は､細かい指定をするほどコードが長くなります｡

メインのコードから関数などを括りだして別のモジュールにまとめるとよさそうです｡
ただし､その場合は実行しているスクリプトの名前の取得方法が変わります｡
単一のスクリプトでファイル名を取得する方法とは異なるアプローチが必要になります｡


# 記事の概要
- 別のモジュール内で`os.path.basename(__file__)`を実行するとそのモジュール名が返ってきてしまう
(実行中のスクリプトの名前が得られない)
- importするモジュール内で`os.path.abspath(sys.modules['__main__'].__file__)`を使うとよい
- 階層が深くなる場合も問題なさそう



# コードの説明
## うまくいかないケース
`main.py`で実行中のスクリプトの名前を表示します｡

```Python:main.py
import os
print(os.path.basename(__file__))
# main.py
```

同様の処理を別のファイル`sub.py`で関数として定義します｡

```Python:sub.py
import os
def print_basename():
    print(os.path.basename(__file__))
```

この関数を`main.py`でimportして実行すると､
`main.py`ではなく`sub.py`が表示されます｡

```Python:main.py
import sub
sub.print_basename()
# sub.py
```
## うまくいくケース
`sub.py`の中身を書き換えて､再び`main.py`を実行します｡[^1]

```Python:sub.py
import os
import sys
def print_basename():
    print(os.path.abspath(sys.modules['__main__'].__file__))
```
```Python:main.py
import sub
sub.print_basename()
# main.py
```

狙いどおり`main.py`が表示されました｡
ちなみに､`sub.py`自体を実行した場合はちゃんと`sub.py`と表示されます


## 実際にファイルの命名に利用する
実行中のスクリプトの名前を保存先に反映させるには､
たとえば以下のような関数を`sub.py`に定義して､`main.py`で実行します｡ [^2]
[^2]: 参考: [前回の記事](https://qiita.com/mochi_gu_ma/items/5a91d757563dc58e426a)

```Python:sub.py
import os
import sys

abspath = os.path.abspath(sys.modules['__main__'].__file__)
main_name = os.path.basename(abspath).rstrip('.py')

if not os.path.exists(main_name):
    os.mkdir(main_name)

def make_textfile(num):
    for i in range(num):
        with open(f'{main_name}/{i+1:03}_output.txt', mode='w')as f:
            f.write('something')
```

```Python:main.py
import sub
import glob
sub.make_textfile(3)
print(sorted(glob.glob('main/*.txt')))
# ['main/001_output.txt', 'main/002_output.txt', 'main/003_output.txt']
```
これでimportしたモジュールの関数を使う場合でも､
保存するファイルの名前に`main.py`の名前を反映することができました｡

## 階層が深くなる場合
階層が深くなる場合も問題なさそうです｡
```:ディレクトリ構成図
.
├── main.py
├── my_package
│   ├── __init__.py
│   └── src
│       └── my_module.py
└── sub.py
```
```Python:my_module.py
# 先ほどのsub.pyと同じなので省略
```

```Python:main.py
from my_package.src import my_module
my_module.print_basename()
# main.py
```

`__init__.py`内でimportした場合も同様｡
```Python:my_package/__init__.py
from my_package.src.my_module import print_basename
```

```Python:main.py
import my_package
my_package.print_basename()
# main.py
```
## もう少しだけ具体的な例: 動物の研究

架空の例ですが､もう少しだけイメージしやすそうな例を出します｡

動物の調査をしており､各動物に関してレポートをまとめる必要があると仮定します｡

そのために､動物の種類ごとにPythonファイルを作成しました｡
`report.py`には共通の処理をまとめた関数が定義されています｡

```:animal_researchのディレクトリ構成図
animal_research/
├── report.py
├── lion.py
├── cheetah.py
└── leptailurus_serval.py
```

どの動物についても共通する処理は､ある程度パターン化して書きたいところです｡
それぞれの動物の名前を定数として先に定義しておくと､パターン化も楽にすみそうです｡

**でもそれをするのもめんどくさいとします｡**

よって､各ファイルの名前を自動で取得し､動物ごとにフォルダを分けて､
図のファイル名にその動物の名前を挿入してくれるような`report.py`を作ります｡

なお､今回は例の提示が目的のため､作図の具体的な処理はすべて省略します｡

```Python:report.py
import os
import sys

import matplotlib.pyplot as plt

abspath = os.path.abspath(sys.modules['__main__'].__file__)
main_name = os.path.basename(abspath).rstrip('.py')

if not os.path.exists(main_name):
    os.mkdir(main_name)

def make_figure(title):
    fig = plt.figure()
    fig.savefig(f'{main_name}/{main_name}_{title}.png')
```

`report.py`で実行ファイルの名前を取得してくれるので､
各動物ごとの処理が簡潔に書けるようになりました｡
(実際には図の中身のデータを入れないとですが､それも省略します)


```Python:lion.py
import report

report.make_figure('weight_chart')
report.make_figure('height_chart')
report.make_figure('lifespan_chart')
```

`lion.py`のコードを示しましたが､今回は他の動物に関しても全く同じコードです｡


それぞれのスクリプトを実行したあとのディレクトリ構成は以下のとおりです｡

```:animal_researchのディレクトリ構成図
animal_research/
├── Lion
│   ├── Lion_height_chart.png
│   ├── Lion_lifespan_chart.png
│   └── Lion_weight_chart.png
├── cheetah
│   ├── cheetah_height_chart.png
│   ├── cheetah_lifespan_chart.png
│   └── cheetah_weight_chart.png
├── cheetah.py
├── leptailurus serval
│   ├── leptailurus_serval_height_chart.png
│   ├── leptailurus_serval_lifespan_chart.png
│   └── leptailurus_serval_weight_chart.png
├── leptailurus serval.py
├── lion.py
└── report.py
```


# まとめ
- 複数のスクリプトでファイルを書き出すときの命名が楽になった！
- **別モジュール内の関数からでも､実行スクリプトの名前をつけれるようになった！(前回との差分)**
- これで少しは分析も楽になる…かも

# 今回の主題となるコード
<details><summary>コード全文(クリックで表示)</summary>

今回は主題となる処理をする`sub.py`のコードだけ載せておきます｡

```Python:sub.py
import os
import sys

abspath = os.path.abspath(sys.modules['__main__'].__file__)
main_name = os.path.basename(abspath).rstrip('.py')

if not os.path.exists(main_name):
    os.mkdir(main_name)

def make_textfile(num):
    for i in range(num):
        with open(f'{main_name}/{i+1:03}_output.txt', mode='w')as f:
            f.write('something')
```

<details>