perllol - Perl で配列の配列を操作する
Perl で構築する一番簡単なデータ構造は、配列の配列(カジュアルに リストのリストとも呼ばれることがあります)です。 これは理解しやすく、そしてより複雑なデータ構造に対しても 適用することのできるものです。
配列の配列は、通常の古い配列 @AoA のようなものです; これは $AoA[3][2]
のように、二つの添え字で要素を取得することができます。 配列の宣言の例を挙げましょう。
use 5.010; # so we can use say()
# 配列に配列へのリファレンスの配列を代入する
@AoA = (
[ "fred", "barney", "pebbles", "bambam", "dino", ],
[ "george", "jane", "elroy", "judy", ],
[ "homer", "bart", "marge", "maggie", ],
);
say $AoA[2][1];
bart
このとき、外側の括弧が丸括弧であったことに注意すべきです。 これは、上の例では@配列に代入するので丸括弧を使う必要があったためなのです。 もし @AoA ではなくて、単にリファレンスを代入したかったというのであれば、 次のように書くことができます:
# 配列へのリファレンスの配列へのリファレンスを代入する
$ref_to_AoA = [
[ "fred", "barney", "pebbles", "bambam", "dino", ],
[ "george", "jane", "elroy", "judy", ],
[ "homer", "bart", "marge", "maggie", ],
];
say $ref_to_AoA->[2][1];
bart
外側の括弧が変わったことと、アクセスの構文が変わっているということに 注目してください。 これは 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; # WRONG!
あなたが望んでいることをしない理由は、スカラに対するような名前付き配列の 代入は、配列をスカラコンテキストとして扱い、これは単に @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"; # 明示的デリファレンス
こんどはこのデータ構造を出力する番です。 あなたはどうやろうと考えてますか? そうですね、簡単に要素を一つだけ出力したいとするとこうなります:
print $AoA[0][0];
配列の内容全部を出力したいとき、次のようには書けません。
print @AoA; # 間違い
なぜなら、これでは単にリストへのリファレンスが取れるだけで、 perl はそれを自動的に参照外しするようなことはしないからです。 このため、あなたは自分自身でループしなければなりません。 これは外側の添え字に対するループでシェルスタイルの for() を使って 構造全体を出力します。
for $aref ( @AoA ) {
say "\t [ @$aref ],";
}
添え字を記録したいのなら、このようにできます:
for $i ( 0 .. $#AoA ) {
say "\t elt $i is [ @{$AoA[$i]} ],";
}
あるいはこのようなやり方もあります。 内側のループに注目してください。
for $i ( 0 .. $#AoA ) {
for $j ( 0 .. $#{$AoA[$i]} ) {
say "elt $i $j is $AoA[$i][$j]";
}
}
見て判るようにこれは少々複雑です。 しかし、途中で一時変数を使えば簡単にできます:
for $i ( 0 .. $#AoA ) {
$aref = $AoA[$i];
for $j ( 0 .. $#{$aref} ) {
say "elt $i $j is $AoA[$i][$j]";
}
}
うーんまだちょっと見にくいですね。 これでどうでしょう:
for $i ( 0 .. $#AoA ) {
$aref = $AoA[$i];
$n = @$aref - 1;
for $j ( 0 .. $n ) {
say "elt $i $j is $AoA[$i][$j]";
}
}
あなたのデータ構造のためのカスタム print を書くのに疲れたなら、 標準の Dumpvalue モジュールや Data::Dumper モジュールを 見るとよいでしょう。 前者は Perl デバッガが使っていて、後者は Perl コードとしてパース可能なものを 出力します。 例えば:
use v5.14; # using the + prototype, new to v5.14
sub show(+) {
require Dumpvalue;
state $prettily = new Dumpvalue::
tick => q("),
compactDump => 1, # comment these two lines
# out
veryCompact => 1, # if you want a bigger
# dump
;
dumpValue $prettily @_;
}
# Assign a list of array references to an array.
my @AoA = (
[ "fred", "barney" ],
[ "george", "jane", "elroy" ],
[ "homer", "marge", "bart" ],
);
push $AoA[0]->@*, "wilma", "betty";
show @AoA;
これは以下のものを出力します:
0 0..3 "fred" "barney" "wilma" "betty"
1 0..2 "george" "jane" "elroy"
2 0..2 "homer" "marge" "bart"
一方、そうしたいかもしれないと書いた二つの行をコメントアウトすると、 代わりに以下のようなものが出力されます:
0 ARRAY(0x8031d0)
0 "fred"
1 "barney"
2 "wilma"
3 "betty"
1 ARRAY(0x803d40)
0 "george"
1 "jane"
2 "elroy"
2 ARRAY(0x803e10)
0 "homer"
1 "marge"
2 "bart"
多次元配列のスライス(行部分)を取りたいのであれば、ややおかしな添え字付けを する必要があるでしょう。 これは参照外しのための参照の矢印を使った単一の要素に対するものは あるのですが、それに対応するスライス用の便利なものはないのです。
以下は、ループを使った一つの操作をどのように行うかの例です。 変数 @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;
}
Tom Christiansen <tchrist@perl.com>
Last update: Tue Apr 26 18:30:55 MDT 2011