GrasshopperのPython開発で自作モジュールをインポートする

# はじめに

[GrasshopperのPython開発で外部エディターを使う](https://qiita.com/keiwatanabe/items/9eb6a7d6749f5e586f7f)という記事を以前書きましたが、Grasshopperで本格的にPythonのプログラムを開発しようとすると１ファイルではコードが収まらなくなってくるので、自作の関数などは別ファイルに分けておきたくなります。
また画像ファイルやCSVを読み込むとき、プラグインをつくるときなどは相対パスでファイルを参照できるようファイル構成をしっかり管理しておく必要があります。
今回は自作のモジュールや設定ファイルを使ってコードを管理しやすくする方法をご説明します。

### **自作モジュールなどでプログラムを分割するメリット**

- コードが長すぎて管理できなくなるのを防げる
- 自作の関数やクラスの個別テストがしやすくなる
- 設定ファイルをつくることでコードをいじらなくても設定変更ができる
- 別のプロジェクトでも関数を流用しやすくなる
- パッケージ化しておくことでgitなどで管理しやすくなる

# ファイル構成

今回はこちらのファイル構成で説明します。
プロジェクトフォルダ直下に```gh_file.gh```と```function.py```があり、```module```というフォルダに```mod.py```とモジュールを呼び出すための```__init__.py```をつくります。
また設定ファイルとして```data```というフォルダの中に```radius_range.json```をつくります。

```
.
├── module
│   └── __init__.py
│   └── mod.py
├── data
│   └── radius_range.json
└── function.py
└── gh_file.gh
```

今回のサンプルプロジェクトでは下記のようなプログラムを例に説明します。

- GHでPop2Dを使って複数の点をランダムな場所に発生させる
- function.pyに点のリストを入力する
- function.pyからmod.pyの関数を呼び出す
- radius_range.jsonで設定された範囲で各点ごとにランダムな半径を返す
- function.pyで各点を中心としてランダムな半径の円を生成

### 設定ファイル

設定ファイル```radius_range.json```ではrandom関数に入れる「最小値」「最大値」「ステップ」を定義します。

```json:radius_range.json
{
    "min": 1,
    "max": 10,
    "step": 1
}
```

### モジュール

モジュール```mod.py```には設定ファイルのjsonを呼びしてランダムな数を返すだけの簡単な関数を作っておきます。この```random_radius```という関数を```function.py```から呼び出そうと思います。

```python:mod.py
# -*- coding: utf-8 -*-
import json
from io import open
import random

def random_radius():
    # jsonファイルの相対パス
    path = "./data/radius_range.json"
    # jsonファイルを開く
    with open(path, encoding='utf-8') as f:
        d = json.load(f)
    # 設定ファイルからパラメータを取得
    min = d['min']
    max = d['max']
    step = d['step']
    # ランダムな数値を返す
    return random.randrange(min,max,step)
```

# Grasshopperの設定

GHファイル```gh_file.gh```はこのように設定しておきます。
```Read File```ではFを右クリック→```Select one existing file```から```function.py```を指定します。
外部ファイルを読み込む設定は[GrasshopperのPython開発で外部エディターを使う](https://qiita.com/keiwatanabe/items/9eb6a7d6749f5e586f7f)を参考にしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263714/1f8b46bc-c4e4-a41d-84e7-474c58cd383f.png)

# GHPythonファイルのディレクトリ

さて本題ですが、この記事を書いている理由を理解いただくために、function.pyで次のコードを試してみましょう。

```python:function.py
import rhinoscriptsyntax as rs
from module import mod

a = mod.random_radius()
```

すると、こんなエラーが出てくると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263714/7b4a9e50-5952-dcdc-e4f3-ed1921b474a7.png)


```
1. Solution exception:パス 'C:\Program Files\Rhino 6\System\data\radius_range.json' の一部が見つかりませんでした。
```

エラーメッセージを読むと```radius_range.json```が見つかりません、と書いてあります。
jsonファイルを探すところまで行っているので```mod.py```は呼び出せているけど、jsonファイルは```Program Files```を探しに行ってしまっているみたいです。
同一ディレクトリにあるGHファイルから、同一ディレクトリにある```function.py```を呼び出しているのに、なぜか```Program Files```を参照している。。

**mod.pyは正しく読み込めてるのに、なぜradius_range.json正しく参照できないのか？**

# モジュール検索パスとカレントディレクトリ

この原因を理解するには**モジュール検索パス**と**カレントディレクトリ**を理解する必要があります。

### モジュール検索パス

これはPythonが標準のライブラリや外部ライブラリをインポートするときに探しに行くパスのリストです。
これを確認するために、同じGHファイル内で下記のコードを試してみます。

```python
import sys
print(sys.path)
```

するとこのように返ってきます。
この先頭にある```'Z:\\Projects\\demo-ghpython-package'```は私がこのプロジェクトファイルを保存したディレクトリであり、GHファイルが保存されている場所です。

```
['Z:\\Projects\\demo-ghpython-package', 'C:\\Program Files\\Rhino 6\\Plug-ins\\IronPython\\Lib', 'C:\\Users\\[username]\\AppData\\Roaming\\McNeel\\Rhinoceros\\6.0\\Plug-ins\\IronPython (814d908a-e25c-493d-97e9-ee3861957f49)\\settings\\lib', 'C:\\Users\\[username]\\AppData\\Roaming\\McNeel\\Rhinoceros\\6.0\\scripts']
```

つまり、GHファイルと同じディレクトリにあるモジュールなどはこのパスをたどればimportできるということです。
これで```mod.py```がimportできた理由がわかりました。

### カレントディレクトリ

カレントディレクトリはPythonが実行されている場所です。
これを確認するために、先程同様同じGHファイル内で下記のコードを試してみます。

```python
import os
print(os.getcwd())
```

すると私の場合は下記のように結果が返ってきます。

```
C:\Program Files\Rhino 6\System
```

どうやら```Program Files```のRhino 6内でGHファイルのPythonコードが実行されていたみたいです。
jsonファイルを参照する際に```./data/~~.json```のように**相対パス**で記述していたので、上記パス内のdataディレクトリにあるjsonファイルを探しにいってしまい、そんなファイルありません！とエラーが出たということみたいです。

# 正しく外部ファイルを読み込むには

方法は２つあります。

1. 外部ファイルは絶対パスで指定する
2. カレントディレクトリを移動する

絶対パスで指定したほうがいい場面もあるとは思いますが、今回のように同じプロジェクトファイル内にデータを格納している場合や別のPCでも同じコードを実行する可能性がある場合は機能しません。
したがって今回は２の方法を使います。

### カレントディレクトリを移動する

カレントディレクトリを移動するために```function.py```に下記のように変更します。

```python:function.py
import rhinoscriptsyntax as rs
from module import mod
import os

# GHファイルのあるディレクトリ＝プロジェクトフォルダを取得
base_dir = os.path.dirname(ghdoc.Path)
# カレントディレクトリをプロジェクトフォルダに移動
os.chdir(base_dir)

a = mod.random_radius()
```

- ```ghdoc.Path```は実行されているGHファイルのパスを返すので、そのパスのディレクトリパスが今回の場合プロジェクトフォルダのパスになるので、それを```base_dir```とします。
- ```os.chdir```で```base_dir```にカレントディレクトリを移動します。

すると無事aからランダムな数字が返ってくるようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263714/51ac3a23-fa12-13c5-1b34-1f82fc90c275.png)


# 円を描くプログラムを追加

もともとaから出力したいのは円オブジェクトだったので、返ってきたランダムな数字を半径とする円を書くようにコードを変更します。

```python:function.py
import rhinoscriptsyntax as rs
from module import mod
import os

# GHファイルのあるディレクトリ＝プロジェクトフォルダを取得
base_dir = os.path.dirname(ghdoc.Path)
# カレントディレクトリをプロジェクトフォルダに移動
os.chdir(base_dir)

circles = []
for pt in pts:
    # 円を描くplaneを定義
    plane = rs.MovePlane(rs.WorldXYPlane(), pt)
    # ランダムな半径を取得
    radius = mod.random_radius()
    # 円を生成してリストに追加
    circles.append(rs.AddCircle(plane,radius))

a = circles
```

これでやっと円が返ってきました。
頑張った割には出来上がりは地味ですね笑
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263714/afbefa64-cc9f-9680-d584-a6ab71e657e4.png)


# さいごに

今回はランダムの範囲を設定ファイルに書いてそれをモジュールを使って読み込んでメインのプログラムに渡すという地味なプログラムでしたが、画像ファイルや大量のCSVなどを読み込むときには大変有効です。
Pythonでプラグインをつくるときなどにご参考にしてください。

# 参考

[https://note.nkmk.me/python-os-getcwd-chdir/](https://note.nkmk.me/python-os-getcwd-chdir/)

[https://note.nkmk.me/python-import-module-search-path/](https://note.nkmk.me/python-import-module-search-path/)