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"; # 明示的デリファレンス
Perl 5.14 以前では、これはコンパイルも出来ませんでした:
push $AoA[0], "wilma", "betty"; # 暗黙的デリファレンス
なぜでしょうか? それは以前は push() の引数は参照ではなく、実際の配列である必要が あったからです。 これはもはや正しくはありません。 実際、上述の「暗黙的デリファレンス」と書かれた行は -- この場合は -- 正しく動作します; デリファレンスが行われます。
「この場合は」と言った理由は、$AoA[0]
が既に配列リファレンスを 保持している場合 にのみ 動作するからです。 未定義値に対して行おうとすると、例外が発生します。 これは、暗黙的デリファレンスは、@{ }
が常に行うような未定義値の 自動有効化を行わないからです:
my $aref = undef;
push $aref, qw(some more values); # WRONG!
push @$aref, qw(a few more); # ok
この新しい暗黙的デリファレンスの振る舞いの利点を使いたいなら、正しく 前に進んでください: これにより目と手に優しいコードになります。 単に古いリリースではコンパイル中にエラーが出ることを理解してください。 特定のリリース以降の Perl 以降でのみ動作し、それより前では動作しないような ものを使う時はいつでも、以下のように目立つように:
use v5.14; # needed for implicit deref of array refs by array ops
ファイルの先頭にこの指示子を書くべきです。 これにより、誰かが新しいコードを古い perl で実行しようとしたときに、 次のようなエラーではなく:
Type of arg 1 to push must be array (not array element) at /tmp/a line 8, near ""betty";"
Execution of /tmp/a aborted due to compilation errors.
次のように親切な情報が出力されます:
Perl v5.14.0 required--this is only v5.12.3, stopped at /tmp/a line 1.
BEGIN failed--compilation aborted at /tmp/a line 1.
こんどはこのデータ構造を出力する番です。 あなたはどうやろうと考えてますか? そうですね、簡単に要素を一つだけ出力したいとするとこうなります:
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];
または少し空白を入れると:
@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