はじめに
GrasshopperのPython開発で外部エディターを使うという記事を以前書きましたが、Grasshopperで本格的にPythonのプログラムを開発しようとすると1ファイルではコードが収まらなくなってくるので、自作の関数などは別ファイルに分けておきたくなります。
また画像ファイルや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関数に入れる「最小値」「最大値」「ステップ」を定義します。
{
"min": 1,
"max": 10,
"step": 1
}
モジュール
モジュールmod.py
には設定ファイルのjsonを呼びしてランダムな数を返すだけの簡単な関数を作っておきます。このrandom_radius
という関数をfunction.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開発で外部エディターを使うを参考にしてください。
GHPythonファイルのディレクトリ
さて本題ですが、この記事を書いている理由を理解いただくために、function.pyで次のコードを試してみましょう。
import rhinoscriptsyntax as rs
from module import mod
a = mod.random_radius()
すると、こんなエラーが出てくると思います。
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ファイル内で下記のコードを試してみます。
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ファイル内で下記のコードを試してみます。
import os
print(os.getcwd())
すると私の場合は下記のように結果が返ってきます。
C:\Program Files\Rhino 6\System
どうやらProgram Files
のRhino 6内でGHファイルのPythonコードが実行されていたみたいです。
jsonファイルを参照する際に./data/~~.json
のように相対パスで記述していたので、上記パス内のdataディレクトリにあるjsonファイルを探しにいってしまい、そんなファイルありません!とエラーが出たということみたいです。
正しく外部ファイルを読み込むには
方法は2つあります。
- 外部ファイルは絶対パスで指定する
- カレントディレクトリを移動する
絶対パスで指定したほうがいい場面もあるとは思いますが、今回のように同じプロジェクトファイル内にデータを格納している場合や別のPCでも同じコードを実行する可能性がある場合は機能しません。
したがって今回は2の方法を使います。
カレントディレクトリを移動する
カレントディレクトリを移動するために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からランダムな数字が返ってくるようになりました。
円を描くプログラムを追加
もともとaから出力したいのは円オブジェクトだったので、返ってきたランダムな数字を半径とする円を書くようにコードを変更します。
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
これでやっと円が返ってきました。
頑張った割には出来上がりは地味ですね笑
さいごに
今回はランダムの範囲を設定ファイルに書いてそれをモジュールを使って読み込んでメインのプログラムに渡すという地味なプログラムでしたが、画像ファイルや大量のCSVなどを読み込むときには大変有効です。
Pythonでプラグインをつくるときなどにご参考にしてください。