perllol - Perl で配列の配列を操作する
組み立てるのが最も単純なことは、配列の配列(不正確にリストのリストとも 呼ばれることがあります)です。 これは理解しやすく、そしてより複雑なデータ構造に対しても 適用することのできるものです。
配列の配列は、あなたが望めば通常の古い配列 @AoA のようなものです。 これは $AoA[3][2]
のように、二つの添え字で要素を取得することができます。 配列の宣言の例を挙げましょう。
# 配列に配列へのリファレンスの配列を代入する
@AoA = (
[ "fred", "barney" ],
[ "george", "jane", "elroy" ],
[ "homer", "marge", "bart" ],
);
print $AoA[2][2];
bart
このとき、外側の括弧が丸括弧であったことに注意すべきです。 これは、上の例では@配列に代入するので丸括弧を使う必要があったためなのです。 もし @AoA ではなくて、単にリファレンスを代入したかったというのであれば、 次のように書くことができます:
# 配列へのリファレンスの配列へのリファレンスを代入する
$ref_to_AoA = [
[ "fred", "barney", "pebbles", "bambam", "dino", ],
[ "homer", "bart", "marge", "maggie", ],
[ "george", "jane", "elroy", "judy", ],
];
print $ref_to_AoA->[2][2];
外側の括弧が変わったことと、アクセスの構文が変わっているということに 注目してください。 これは C とは違って、Perl では配列と参照とを自由に交換できないからです。 $ref_to_AoA は配列への参照です。 その配列は @AoA で、これがまた配列です。 同様に、$AoA[2]
は配列ではなく配列への参照です。 ですから:
$AoA[2][2]
$ref_to_AoA->[2][2]
これは、以下のような書き方でも同じことになります:
$AoA[2]->[2]
$ref_to_AoA->[2]->[2]
この規則は隣り合ったかっこ(それが大かっこだろうが中かっこだろうが) だけのものなので、参照外しをする矢印を自由に省略できます。 けれども一番最初にある矢印だけは、それがリファレンスを保持する スカラであるために省略することはできません。 これは $ref_to_AoA が常に必要とするものです。
固定的なデータ構造の宣言は良いのですが、その場で新しい要素を 追加したいとき、あるいは完全に 0 から作り上げたいときにはどうするのでしょう?
まず最初に、ファイルから読み込むことを見てみましょう。 これは一度に一つの行を追加していくようなものです。 私たちはここで、読み込んでいるファイルが、一行(line)が一つの行(row)に 対応し、各単語が要素に対応しているようなフラットなファイルであると 仮定しています。 もし配列 @AoA にそういった物を設定しようとするのであれば、 それは以下のようなやり方になります:
while (<>) {
@tmp = split;
push @AoA, [ @tmp ];
}
関数を使って読み込むこともできます:
for $i ( 1 .. 10 ) {
$AoA[$i] = [ somefunc($i) ];
}
あるいは、配列に設定するために使う一時変数を使うこともできます。
for $i ( 1 .. 10 ) {
@tmp = somefunc($i);
$AoA[$i] = [ @tmp ];
}
配列への参照のコンストラクターである []
を使うことが非常に重要です。 次のように書いてしまうのはとてもまずいやりかたです:
$AoA[$i] = @tmp;
このようなスカラに対する名前付き配列の代入では、@tmp にある要素の数を 数えてその数を代入します。 そしてこれはおそらくはあなたの望んだことではないでしょう。
use strict
の元で実行するのであれば、以下の様にちょっと宣言を 付け加えるとよいでしょう:
use strict;
my(@AoA, @tmp);
while (<>) {
@tmp = split;
push @AoA, [ @tmp ];
}
もちろん、一時的な配列もなければならないというものではありません:
while (<>) {
push @AoA, [ split ];
}
また、push() を使わなくてもできます。 どこに押し込めたいかと言うことがわかっているのなら、直接 代入させることもできます:
my (@AoA, $i, $line);
for $i ( 0 .. 10 ) {
$line = <>;
$AoA[$i] = [ split ' ', $line ];
}
あるいはこういう風にもできます:
my (@AoA, $i);
for $i ( 0 .. 10 ) {
$AoA[$i] = [ split ' ', <> ];
}
本当にそうしたいときを除き、スカラコンテキストでリストを返すかもしれない 関数を使ってしまう可能性に気をつけるべきです。 これは普通の読み手には明らかでしょう:
my (@AoA, $i);
for $i ( 0 .. 10 ) {
$AoA[$i] = [ split ' ', scalar(<>) ];
}
配列へのリファレンスとして変数 $ref_to_AoA を使いたいというのであれば、 以下の様にする必要があるでしょう:
while (<>) {
push @$ref_to_AoA, [ split ];
}
これで新しい行を追加することができます。 新しいカラムを追加するのは? あなたがまさに行列を扱っているのなら、大概は単純な代入となります:
for $x (1 .. 10) {
for $y (1 .. 10) {
$AoA[$x][$y] = func($x, $y);
}
}
for $x ( 3, 7, 9 ) {
$AoA[$x][20] += func2($x);
}
これは対象となる要素が既に存在しているかどうかには影響されません: (ない場合でも)喜んであなたのためにその要素を作り出し、必要に応じて 間にある要素に undef
をセットします。
あなたは、単に行に追加したいだけという場合であっても、 ちょっと妙に見えることをしなければならないでしょう:
# add new columns to an existing row
push @{ $AoA[0] }, "wilma", "betty";
次のようには 書けない ことに注意してください。
push $AoA[0], "wilma", "betty"; # 間違い!
事実、これはコンパイルすらできません。 なぜでしょうか? それは push() の引数は参照ではなく、実際の配列でなければならないからです。
こんどはこのデータ構造を出力する番です。 あなたはどうやろうと考えてますか? そうですね、簡単に要素を一つだけ出力したいとするとこうなります:
print $AoA[0][0];
配列の内容全部を出力したいとき、次のようには書けません。
print @AoA; # 間違い
なぜなら、これでは単にリストへのリファレンスが取れるだけで、 perl はそれを自動的に参照外しするようなことはしないからです。 このため、あなたは自分自身でループしなければなりません。 これは外側の添え字に対するループでシェルスタイルの for() を使って 構造全体を出力します。
for $aref ( @AoA ) {
print "\t [ @$aref ],\n";
}
添え字を記録したいのなら、このようにできます:
for $i ( 0 .. $#AoA ) {
print "\t elt $i is [ @{$AoA[$i]} ],\n";
}
あるいはこのようなやり方もあります。 内側のループに注目してください。
for $i ( 0 .. $#AoA ) {
for $j ( 0 .. $#{$AoA[$i]} ) {
print "elt $i $j is $AoA[$i][$j]\n";
}
}
見て判るようにこれは少々複雑です。 しかし、途中で一時変数を使えば簡単にできます:
for $i ( 0 .. $#AoA ) {
$aref = $AoA[$i];
for $j ( 0 .. $#{$aref} ) {
print "elt $i $j is $AoA[$i][$j]\n";
}
}
うーんまだちょっと見にくいですね。 これでどうでしょう:
for $i ( 0 .. $#AoA ) {
$aref = $AoA[$i];
$n = @$aref - 1;
for $j ( 0 .. $n ) {
print "elt $i $j is $AoA[$i][$j]\n";
}
}
多次元配列のスライス(行部分)を取りたいのであれば、ややおかしな添え字付けを する必要があるでしょう。 これは参照外しのための参照の矢印を使った単一の要素に対するものは あるのですが、それに対応するスライス用の便利なものはないのです。 (もちろん、スライス操作をするためにループを書くことは常に 可能だと言うことを忘れないで下さい。)
以下は、ループを使った一つの操作をどのように行うかの例です。 変数 @AoA が前のものと同じであると仮定しています。
@part = ();
$x = 4;
for ($y = 7; $y < 13; $y++) {
push @part, $AoA[$x][$y];
}
このループをスライス演算に置き換えることができます:
@part = @{ $AoA[4] } [ 7..12 ];
あなたも見て感じるかもしれませんが、これは読み手にはちょっと不親切です。
あー、でも、$x を 4..8、$y を 7 から 12 とするような 二次元のスライス を 必要とするときには? うーん、単純なやり方はこうでしょう。
@newAoA = ();
for ($startx = $x = 4; $x <= 8; $x++) {
for ($starty = $y = 7; $y <= 12; $y++) {
$newAoA[$x - $startx][$y - $starty] = $AoA[$x][$y];
}
}
スライスを使ってループを簡単にできます:
for ($x = 4; $x <= 8; $x++) {
push @newAoA, [ @{ $AoA[$x] } [ 7..12 ] ];
}
あなたがシュワルツ変換に興味を持っているのなら、map を使って次のように することを選ぶかもしれません:
@newAoA = map { [ @{ $AoA[$_] } [ 7..12 ] ] } 4 .. 8;
あなたの上司が不可解なコードによるジョブセキュリティ(もしくは急激な不安)の 追求を非難していたとしても、説得するのは難しいでしょうね :-) もし私があなたの立場だったら、こういった操作は関数に押し込めるでしょう。
@newAoA = splice_2D( \@AoA, 4 => 8, 7 => 12 );
sub splice_2D {
my $lrr = shift; # リファレンスのリストのリストへのリファレンス!
my ($x_lo, $x_hi,
$y_lo, $y_hi) = @_;
return map {
[ @{ $lrr->[$_] } [ $y_lo .. $y_hi ] ]
} $x_lo .. $x_hi;
}
perldata(1), perlref(1), perldsc(1)
Tom Christiansen <tchrist@perl.com>
Last update: Thu Jun 4 16:16:23 MDT 1998