| Top Page | プログラミング | Perl 目次 | prev | next | 索引 |
アルゴリズム(計算手順)+データ構造がプログラムだと言われます. アルゴリズムを考えることだけではなく,データ構造の設計も プログラムの設計の重要な要素です.
データ構造とは,「複数のデータを結びつけて管理するときの,まとめ方の構造」 とでも言ったらよいでしょうか.たとえば,配列は複数のデータを順番に並んだ ものとしてまとめて管理するひとつのデータ構造ですし,ハッシュは キーになる文字列とそれに対応する値というペアを,順不同でまとめて 管理するデータ構造です. 配列もハッシュもなしでプログラムを書くとなるとどんなに不自由かを考えれば, データ構造の重要性は想像がつくでしょうか. 表計算ソフト(Excelなど)は,2次元配列というデータ構造が基本になってますね.
では,配列とハッシュだけ(あるいは2次元配列だけ)あればすべて済むかというと, そんなことはありません.扱うデータの性質によっては, もっと複雑なデータ構造が必要になります. 不適切なデータ構造でむりやり書こうとすると,こんがらがったプログラムに なってしまいがちです. はじめに適切なデータ構造を設計すると,プログラムがずっとシンプルに 見通しよく書けることがよくあります.
このページでは,単純な配列やハッシュよりも複雑な(でも現実世界 のデータを扱おうとするとぜひ欲しくなる)データ構造を,リファレンス を使って実現する方法を紹介します.
こんなデータファイルがあるとします. 3次元空間内の座標を,x, y, z という3つのデータの組みで表現したもので, たくさんの点の座標が何行にもわたって並んでいます.
0.5 -0.3 7.8 1.8 0.2 8.9 1.1 -1.5 5.3 2.1 0.1 3.5このデータを読み込んで処理するプログラムでは,1点ごとのデータをひとまとまり として,それらの配列としてデータを管理できれば便利でしょう. 個々の点のデータは,要素が3つの配列ということになります.
でも配列の配列は作れない,そこで「ひとつの点のデータを持った要素数3の配列」 へのリファレンス(すなわちスカラー)の配列を作ることにします.
まず,失敗例から.
@point_array = (); # 点のデータへのリファレンスをしまう配列 while ($line = <>) { # コマンドラインで指定したファイルから一行ずつ読む chomp $line; ($x, $y, $z) = split /\s+/, $line; # データを分割して $x, $y, $z に代入. @point = ($x, $y, $z); # @point にこれらの値のリストを代入. # この時点で,$point[0] は $x, # $point[1] は $y, $point[2] は $z. # 配列 @point へのリファレンスを @point_array の末尾に加える push @point_array, \@point; } foreach $ref_p (@point_array) { # @point_array の各要素すなわちリファレンスについて, # そのリファレンスが指す配列の各要素を表示する. print $ref_p->[0], "\t", $ref_p->[1], "\t", $ref_p->[2], "\n"; }
これを実行すると,こんな出力が得られます.
2.1 0.1 3.5 2.1 0.1 3.5 2.1 0.1 3.5 2.1 0.1 3.5
データファイルの最後の1点のデータが繰り返し表示されていて,それより前の 点のデータは消えてしまってます.なぜでしょう?
push @point_array, \@point; という文で配列 @point_array に加えているのは, @point という配列変数へのリファレンスです.変数とはメモリー上の領域であり, リファレンスはその領域のありかを示すのでした.
だから,上のプログラムでは @point という名前の変数の_場所_を繰り返し 点のデータの数だけ重複して配列 @point_array 格納しています. その場所に記録されている_値_はどこにもしまわれていません.
while ($line = <>) {} ループの実行が終わった時点で @point に記録 されているのは最後の一行のデータです.@point_array の全要素はすべて等しく @point のありかを指し示していますから,この時点で @point に記録されている 最後のデータが繰り返し表示されることになるのです.
では,どうやったらほんとに配列の配列を作ることができるのでしょうか. 一点ごとに違う名前の配列にしまい,その配列へのリファレンスを @point_array に push していけばよいのですが,何点あるかわからないものにいちいち変数名 を与えることはできません.
ここで登場するのが無名配列です. 無名配列は,メモリー上に領域を確保してそこに値を記録できるけれど, 変数名を持っていない配列です. いちいち名前をつけることなく,リファレンスだけ作って記録していけば, 配列の配列を作るという当初の目的が果たせます.
# リストを [ ] で囲むと無名配列が作られる.これをスカラーに代入すると, # その無名配列へのリファレンスがスカラー変数に記録される. $ref_a = [10, 20, 30, 40]; foreach $x (@$ref_a) { # foreach で無名配列の全要素を表示 print $x, "\t"; } for ($i = 0; $i < 4; ++$i) { # 無名配列の各要素を添え字で指定 # $ref_a->[$i] のかわりに $$ref_a[$i] と書いても同じ print $ref_a->[$i], "\t"; }
ふつうの配列変数にリストを代入するときには, $a = (10, 20, 30, 40); のように書きますが,無名配列を作って それへのリファレンスを知りたときには,[ ] で囲って $ref_a = [10, 20, 30, 40]; と書きます.
この手を使って最初の失敗を書きなおしてみます.
@point_array = (); # 点のデータへのリファレンスをしまう配列 while ($line = <>) { # コマンドラインで指定したファイルから一行ずつ読む chomp $line; ($x, $y, $z) = split /\s+/, $line; # データを分割して $x, $y, $z に代入. $ref = [$x, $y, $z]; # あらたに無名配列を生成しリファレンスを $ref に. push @point_array, $ref; # リファレンスを配列 @point_array の末尾に追加. } foreach $ref_p (@point_array) { # @point_array の各要素すなわちリファレンスについて, # そのリファレンスが指す配列の各要素を表示する. print $ref_p->[0], "\t", $ref_p->[1], "\t", $ref_p->[2], "\n"; }こんどの出力はこうなります.
0.5 -0.3 7.8 1.8 0.2 8.9 1.1 -1.5 5.3 2.1 0.1 3.5
ちゃんと全部のデータが表示されましたね. あとは,読み込んだデータを目的に応じて処理するプログラムを書くだけです.
こんどはハッシュの値として 無名配列へのリファレンスをしまってみましょう. 文字列をキーにして,データの配列を取り出せるようにしようという狙いです.
リファレンスの最初のページの,一番はじめの データを使います. 一行ごとに,実験条件を表す文字列と,その条件で得られた複数のデータが 書かれたデータファイルです.
L100N100 10.5 10.8 12.5 10.5 L100N010 5.5 5.2 7.1 6.8 L020N100 8.1 8.8 6.6 7.9 L020N010 5.1 4.5 4.8 4.5 L005N100 3.2 3.1 2.5 3.6 L005N010 3.1 2.9 2.6 3.2
このデータから,実験条件の名前をキー,その条件で得られたデータの配列への リファレンスを値とするハッシュを作ってみましょう.
%data_table = (); # このハッシュでデータを管理する. while ($line = <>) { # コマンドラインで指定したファイルから一行ずつ読む chomp $line; # 分割して作られた文字列の最初のものが $ condition に, # 残りは配列 @data に記録される. # うえのデータの場合,最初の文字列が $condition に,残りの数値のならびが # 配列にしまわれる. ($condition, @data) = split /\s+/, $line; $ref = [ @data ]; # @data と同じ内容の無名配列を生成し, # その無名配列へのリファレンスを $ref に. $data_table{$condition} = $ref; # 実験条件がキー,配列へのリファレンスが値 } foreach $condition (keys %data_table) { # ハッシュの各キーについて, print $condition; # キーを出力. $ref_data = $data_table{$condition}; # 対応する値は配列へのリファレンス. foreach $x ( @$ref_data ) { # 配列 @$ref_data の各要素を表示. print "\t", $x; # } print "\n"; }
こんどは,スカラーのリストではなくて配列変数 @data を [] で囲って 無名配列を作っています.$ref は @data へのリファレンスではなく, @data のデータを使って新たに生成された無名配列へのリファレンスで あることに注意してください.@data へのリファレンスだったら,最初の 失敗例と同じになってしまいます.
($condition, @data) = split /\s+/, $line; という書き方はこれまで でてきませんでしたが,コメントを見れば意味するところは分かるでしょう. このようにスカラー変数と配列変数をあわせて () で囲ったものに リストや配列を代入すると,右辺の先頭の要素から順にスカラー変数に 代入し,残りはすべて配列変数に代入されます.
上のプログラムの後半で,ハッシュ %data_table にしまわれている '配列へのリファレンス'を参照して,その配列の個々の要素の値を見るところが あります.ここでは,一度 '配列へのリファレンス' を変数 $ref_data に代入してから, foreach $x ( @$ref_data ) {... という foreach 文を使って各要素にアクセスしています.
変数 $ref_data に代入せず,直接 $data_table{$condition} に @ をつけてみたらどうでしょうか.
# 失敗例 %data_table = (); # このハッシュでデータを管理する. # (中略...) foreach $condition (keys %data_table) { # ハッシュの各キーについて, print $condition; # キーを出力. # $data_table{$condition}が参照する配列の各要素を出力. foreach $x ( @$data_table{$condition} ) { print "\t", $x; } print "\n"; }
実行してみると,データは表示されません. これはなぜかというと,@$data_table{$condition} が, 「 $data_table{$condition} のあたまに @ をつけたもの」 とは解釈されずに, 「@$data_table のうしろに {$condition} をつけたもの」と解釈されてしまうからです. 意図通りに解釈してもらうには, $data_table{$condition}を {} で囲ってから @ をつける, つまり @{$data_table{$condition}} と書きます. 以下のプログラムは期待した通りに動いてくれます.
%data_table = (); # このハッシュでデータを管理する. # (中略...) foreach $condition (keys %data_table) { # ハッシュの各キーについて, print $condition; # キーを出力. # $data_table{$condition}が参照する配列の,各要素を出力. foreach $x ( @{$data_table{$condition}} ) { print "\t", $x; } print "\n"; }
このように,変数名の解釈の優先順位を示すには {} を使います. 変数展開での変数名の範囲の示し方と似ていますね. 演算の優先順位を示す () ではだめです.
最初にも書いたとおり,データ構造をじょうずにデザインすることは とても大切です.よいデータ構造の設計があってこそ,よいプログラムが 書けます.プログラム=アルゴリズム(計算手順)ではなくて, プログラム=アルゴリズム+データ構造であるという先人の言葉 (by N. Wirth) を胸に,プログラムを書きはじめる前に,どんな形でデータを扱うかを よく考えましょう.これができるようになれば,ワンランクアップです.
次のページがリファレンスの最後です.今度は無名ハッシュをつかった データ構造のお話です.