LoginSignup
7
6

More than 3 years have passed since last update.

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

Posted at

はじめに

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

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

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

ファイル構成

今回はこちらのファイル構成で説明します。
プロジェクトフォルダ直下にgh_file.ghfunction.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関数に入れる「最小値」「最大値」「ステップ」を定義します。

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

モジュール

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

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開発で外部エディターを使うを参考にしてください。
image.png

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

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

function.py
import rhinoscriptsyntax as rs
from module import mod

a = mod.random_radius()

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

image.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ファイル内で下記のコードを試してみます。

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つあります。

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

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

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

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

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.chdirbase_dirにカレントディレクトリを移動します。

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

image.png

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

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

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

さいごに

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

参考

https://note.nkmk.me/python-os-getcwd-chdir/

https://note.nkmk.me/python-import-module-search-path/

7
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6