2014年8月28日木曜日

matlab: 構造体の利用例: ワークスペース上の変数を一つの構造体にまとめる

matlab で作業していると,たくさんの変数が出来てきて,とりあえず一つにまとめておきたいことがある.また,関数を作っていて,デバッグのために,関数内の変数をすべて戻り値として返したいことがある.その際は,いちいちどの変数を返すか指定するのは面倒なので,全部まとめて一つにして返したくなる.

これらに共通して行いたいことは,ワークスペース workspace 上の変数を,一つにまとめたいということだ.何にまとめるか?それは,構造体にまとめればよい.

では,行ってみよう.


まずはわかりやすい実装を順を追って示す.
z, n, Z をワークスペースで使っていない変数とする.これらの変数名を使っている場合,以下の手順では,使っていない変数に置き換えて試すこと.でないとこれらの変数を上書きしてしまう.

いま,ワークスペースに a, b という変数があると仮定しよう.
a = rand(3)
b = 'hello, world!'
とでもすれば手軽に試せる.

ワークスペース内の変数のリストを取得するには,who あるいは whos を使う.
who は単純に変数名のリストを文字列のセル配列の形で返す.
whos は変数の詳細な情報を構造体配列の形で返す.

今回は who を使うのが簡単だ.

z = who;

z は文字列のセル配列になる.

>> z
z =
    'a'
    'b'

さて,ワークスペースの変数のまとめ先を構造体 Z としよう.Z には,各変数名(=zの各セルの内容)をフィールド名として追加し,そのフィールドの中身には,実際の変数の中身を代入する.これには,「変数によるフィールド名の参照」と「eval による中身の評価」という2つの技を使う.たとえば,1番目の変数についてやってみよう.

Z.(z{1}) = eval(z{1});

左辺で,フィールド参照のピリオド . のあとに小括弧 () を使っていて,その中で z{1} を呼び出している.また,右辺では,z{1} を呼び出した結果を関数 eval で評価している.いま,z{1} は文字列としての a なので(z{1} = 'a'),これは以下の文と等価である.

Z.a = a;

これがまさにやりたかったことだった.
少し説明すると,左辺はまず
Z.('a')
と解釈される.構造体フィールドの参照に小括弧 () を使うと,中の文字列がフィールド名として解釈される.つまり Z.('a') は Z.a と等価である.文字列のところは,もちろん中身が文字列の変数を利用できる.今回は z{1} を用いた.これが「変数によるフィールド名の参照」である.

次に,右辺は
eval('a')
と解釈される.これにより,中の文字列が文として評価される.つまりコマンドラインで a を実行するのと等価である.>> a と >> eval('a') が等価ということだ.eval の場合,その結果の戻り値を関数 eval の戻り値として返してくれる.今回の場合,a の中身そのものを返してくれることになる.これが「eval による中身の評価」である.

あとはループで回せばよい.

for n=1:length(z)
    Z.(z{n}) = eval(z{n});
end

結果を見ると,意図したとおり,ワークスペース変数の内容が Z にまとめて格納されているはずだ.

>> Z
Z =
    a: [3x3 double]
    b: 'hello, world!'

まとめると,素朴なやり方としては,以下のコードでOKだ.

z = who;
for n=1:length(z)
    Z.(z{n}) = eval(z{n});
end

意外と簡単便利にまとまるのである.

~発展1:ループを使わず(かっこよく?)書く~

もう少し凝ると,ループを回さない方法も作れる.cell2structcellfun を使う.

z = who;
Z = cell2struct( cellfun(@eval, z, 'UniformOutput', false), z, 1);

上記では一応変数 z を用いて who の結果を一度受けることにしたが,そのステップは省こうと思えば省ける.

Z = cell2struct( cellfun(@eval, who, 'UniformOutput', false), who, 1);

ただし who を 2 回呼び出すことになる. 2 回の呼び出し間で結果が同一であると保証されていればこれでよく,実際実行した限りは問題なかった.しかし何かの割り込み処理等で who の呼び出し間で変数が増減するとトラブルになる可能性があるので,あまり推奨しない.

~発展2:関数化する~

変数のとりまとめ処理を

Z = gather_v;

のように一発で書ければとても簡単だ.そこで,関数 gather_v として実装してみよう.

ここで問題になるのは,gather_v でまとめたいのは gather_v を呼び出しているワークスペース側の変数たちであって, gather_v の関数内ワークスペースの変数たちではないという点だ.関数内からは,通常,関数の外のワークスペースにはアクセスできない.

この壁に風穴を開ける方法の一つが関数 evalin だ.evalin は,引数として渡された文字列を,指定された workspace 内で実行し,その結果を現在の workspace に返してくれる.指定できる workspace は,'caller' つまり関数の呼び出し元と,'base' つまりコマンドラインの 2 つ.

たとえば,呼び出し元の側の変数のリストを取得するには,

z = evalin('caller', 'who');

とする.同様に,eval を用いていた箇所を evalin に置き換える.ループを使うのであれば

for n=1:length(z)
    Z.(z{n}) = evalin('caller', z{n});
end

ループを使わないのであれば,'caller' を並べたセル配列を予め用意して

ws = repmat({'caller'}, size(z));
Z = cell2struct( cellfun(@evalin, ws, z, 'UniformOutput', false), z, 1);

とすればよい.まとめると,関数としては

gather_v.m:

function Z = gather_v()

z = evalin('caller', 'who');
ws = repmat({'caller'}, size(z));
Z = cell2struct( cellfun(@evalin, ws, z, 'UniformOutput', false), z, 1);


とすればよい.これで,変数名のバッティングに悩む必要もなく,非常に便利になる.

~追記~

今回使った技は,
eval, evalin, cellstruct, cellfun, 構造体のフィールド名の動的参照
といったあたりだ.興味のある読者は,matlab のヘルプなどを参照してより深く使いこなして頂きたい.

当ブログの関連記事を以下に挙げておく.ご参考まで.
~追記2~

who は列ベクトル(縦ベクトル) の形で結果を返す.本記事はそれを前提にして,cell2struct の最後の引数を 1 としている.もし将来この仕様が変わった場合(行ベクトル,横ベクトルを返すように変更された場合)でもエラーを起こさないためには,Z = の右辺で,z のところを z(:) にすればOKだ.

0 件のコメント:

コメントを投稿