AKAGI Rails

鉄道模型シミュレーターで遊んでいたはずが、気づいたらPythonなども。

VRMNXpyで名前空間はどうなっているのか

本稿では、VRMNXpyにおける名前空間、すなわちあるオブジェクトが参照されうる範囲(見える範囲=スコープともいう)がどのようになっているのかについて述べていきます。また、その結果として,どのような実装パターンが良さそうか、についても考察を与えます。

なおAKAGIの理解の範囲内では、Pythonにおいて変数とオブジェクトの違いは曖昧である(ことにしておいたほうが都合がいい)ので、本稿においてもこれらの用語を混同して使用します。混同しているとハマる罠があるのは知っていますが,一旦これは棚上げにします。

ローカルとグローバル

関数 (function) の中で生成されたオブジェクトは「ローカル変数」となり、実行中のその関数の中でのみ参照や書き込みが許され,その関数の処理が終わると廃棄されます。別の関数からはもちろん、一度処理が終わった関数をもう一度呼び出したとしても、以前の実行時の変数の内容は破棄されていて知ることはできません。

一方で関数の外で生成されたオブジェクトはグローバル変数となり、削除しない限りはその後のランタイムのどこからでも参照することができます。ですから,なにか作った値を,将来,別の関数などから参照したいと思っているのなら,何らかのグローバル変数に保存しておく必要があります。

グローバル変数に関数内から何かを書き込む

前述のように,将来,なにかの値を今実行中の関数とは別の関数で使いたい場合には,グローバル変数に保存しておく必要があります。何らかの関数内からグローバル変数の内容を書換えたいときには一手間かかります。

some_global_var = None # これはグローバル変数

def some_function(arg):
    global some_global_var
    # グローバル変数 some_global_var をこの関数内から書き込み可能にする
    
    result = arg * 2
    some_global_var = result # グローバル変数に保存した

このように,関数内でglobal文を用いて,グローバル変数some_global_var と解釈するように明示する必要があります。

VRMNXpyの実践上,注意しておくべき事柄は,イベントハンドラも関数であるという点です。ですから,イベントハンドラ内で vrmapi を通じて取ってきた何らかの値を将来使えるように保存しておきたければ,保管用のグローバル変数イベントハンドラの外で定義する必要があります。

具体例の紹介は省略しますが,グローバル変数を使わない別の方法もあります。それはVRMNXの各オブジェクトが持つdictを使う方法です。

部品を跨ぐコードのスコープ

VRMNXでは,レイアウト上に配置されたセンサーや編成や車両や信号機といった「部品」ごとそれぞれに,Pythonスクリプトを書き込む「スペース」が用意されています。

バージョン5までのVRMスクリプトとは,部品とスコープの扱いがだいぶ変わりました。

早い話, すべての部品は1つのスコープを共有していて,全部品のPythonスクリプトから,全部品のグローバル変数を直接参照可能 です。ただし,いくつかの注意事項があります。なぜなら,VRMNXがビュワー起動までにPythonスクリプトをどう扱っているか,イメージできていないと若干トリッキーだからです。マニュアルにはちゃんと書いてありませんが,実験的なコードをいろいろ書いて試した範囲では,ビュワー起動時に以下のような手順を踏んでいることが推測されます。

  1. ビュワー起動時に,レイアウトオブジェクトのPythonスクリプトが実行される。
    • 注) イベントハンドラを含む関数は,名前が拘束されるだけで中身の実行はまだされない
  2. その他の部品のPythonスクリプトが実行される。(順番は不定らしい。)
  3. レイアウト→その他部品の順で,initイベントがキックされる。
  4. 画面の描画が始まる

(この程度に理解しておけば,今のところVRMNXpyをそれなりに思った通りに動かせます。)

この挙動の中で注意すべきなのは,1や2の最中で,まだ定義されていない別オブジェクトのグローバル変数を参照しようとすると参照エラーが起きる可能性がある点です。この場合,ビュワーがクラッシュすることも想定されますが,一方でVRMNXビュワーが完全に起動していないのでLOGウィンドウが使い物にならないかもしれません。ですから, 初期化部分では不用意な動作を書くべきではありません。 vrmapi も不用意に使用しないほうがいいと思っています。 せいぜい,モジュールのインポートや,初期化だけしておきたいカラの変数の定義をするに留めておくのが良いでしょう。これは,VRMNX用の自作モジュールにも当てはまるでしょう。

ちょっと古い作例ですが,自作踏切モジュールは,踏切部品(親)のPythonスクリプトの初期化部分で定義したグローバル変数を,子の踏切部品からも参照しちゃう作例になっています。(ただしこのような実装は可能ですが,私のVRMNXpyの理解の途上でやってみた実装に過ぎず,別の部品のコードで定義したPythonオブジェクトを参照するという分かりにくさがあるので,常におすすめできる方法ではないと今は考えています。)

VRMNXpyの実装パターンはどうなっていくか

ここまでグローバル変数の話をしましたが, グローバル変数を使わない 方法によりわかりやすい実装をすることができるのではないかと思っています。グローバル変数の代わりに使うのが,レイアウト外部の自作Pythonモジュールです。

撮る夫くんやATENXAは,いろいろなレイアウトに使い回せる汎用性を意識して作っているものですが,レイアウトごとに,ad hocPythonモジュールを用意することはできるでしょう。例えば,

(root)
├ MyLayout.vrmnx
└ mymodule.py

のように自作モジュール (mymodule.py) を用意します。中身は簡単に次のように想定します。

# mymodule.py

my_global_var = None # このグローバル変数をなにかに使おう

レイアウトスクリプトの中で,vrmnxの次にmymoduleをimportします。

# レイアウト
import vrmnx
import mymodule

def vrmevent(obj, ev, param):
    pass # 以下省略

すると,例のグローバル変数は他の部品からは mymodule.my_global_var で参照できます。

このようにして,

というルールを敷いておけば,別の部品のグローバル変数のつもりで参照したり書き込んだりという混乱の元を,いくぶんか減らせるのではないかと思います。

(小規模なレイアウトで小規模なギミックを実装する範囲に留まるのであれば,これに限りませんが。)

さらにグローバル変数に対するアクセスポリシーを厳しくするには,アンダースコア _ で始まる変数名を使うことです。Pythonでは,アンダースコアで始まる変数は「内部ONLY」の意味を持つと暗黙に約束されています。(たとえグローバル変数であっても。)アンダースコアで始まる内部ONLYのグローバル変数は,グローバル変数ながら,モジュールの外部からのアクセスをしてはいけないという明確な表示になります。詳しくは,PEP 8の「実践されている命名方法」などを参照されるといいでしょう。

撮る夫くんでは,内部ONLYのグローバル変数を多用しています。興味のある方は撮る夫くんのソースをご覧になってみてください。

まとめ

あまりまとまりがありませんが,VRMNXpyの変数のスコープについて語ってみました。VRMNXならではの重大事項として,各部品はスコープを共有しているということに注意が必要です。さらに,ビュワー起動時の内部動作がどうなっているかについて多少の想像を及ばせておく必要があります。このほかは,Python一般の知識がきちんと通用すると思いますので,分かりやすく,間違えにくいコードを書いていく努力をしていけばよいものと思われます。