- Perl ›
- here
リファレンスと多次元データ構造
リファレンスとはデータを指し示すものを表現します。C言語のポインタのようなものだと考えると理解しやすいです。
配列のリファレンス
配列のリファレンスについて解説していきます。
配列
配列の作成です。
my @nums = (1, 2, 3);
配列のリファレンスの作成
配列のリファレンスの作成です。リファレンスは「\」記号を使って作成します。
my @nums = (1, 2, 3); my $nums = \@nums;
「リファレンス」とは「指すもの」を意味します。$numsは@numsを指しています。
$nums ---> @nums
無名配列のリファレンスの作成
無名配列のリファレンスの作成です。「[]」は無名配列生成子と呼ばれます。
my $nums = [1, 2, 3];
$numsは、名前を持たない「(1, 2, 3)」という配列を指し示しています。
$nums ---> (1, 2, 3)
無名配列のリファレンスの記述は、配列のリファレンスを簡単に作成するための記述だと考えるとわかりやすいと思います。無名配列生成子を使うと配列のリファレンスを作成するために、配列を作成する必要がなくなります。
# 配列のリファレンスの作成 my @nums = (1, 2, 3); my $nums = \@nums; # 上記の簡略表記 my $nums = [1, 2, 3];
Perlでは配列と配列のリファレンスの両方の表記が出てくることがコードが読みにくいひとつの原因です。「配列」と「配列のリファレンス」をきっちり区別してコードを読むことが大切です。
配列のリファレンスのデリファレンス
配列のリファレンスから配列を取り出すにはデリファレンスという操作を行う必要があります。配列のデリファレンスには「@{}」を使用します。
my @nums = @{$nums};
次のように括弧を省略することもできます。
my @nums = @$nums;
配列の要素を取得するにはデリファレンスしてから取得することもできますが、その方法は少し不便です。Perlでは配列のリファレンスから直接要素を取得する方法が用意されています。配列のリファレンスから要素を取り出すには、矢印演算子「->」を使用します。
my $first = $nums->[0]; my $second = $nums->[1];
以下のように配列の要素を取得する場合と比較してみてください。矢印演算子があるかないかが違いになります。
my $first = $nums[0]; my $second = $nums[1];
Perlでプログミングする場合は、それが配列なのか配列のリファレンスなのかを常に意識してください。
配列のリファレンスについては以下のページでも詳しく解説しています。
二次元配列
Perlで二次元配列を作成してみましょう。Perlの配列の特徴として、要素として持つことができるのはスカラ値のみであるという制限があります。ですから次のように配列の要素として配列を持つことはできません。
# 二次元配列の間違った例 my @person1 = ('Ken', 'Japan', 19); my @person2 = ('Taro', 'USA', 45); my @persons = (@person1, @person2);
@personsは('Ken', 'Japan', 19, 'Taro', 'USA', 45)という一次元の配列になってしまいます。
配列の要素に持つことができるのはスカラ値だけです。配列のリファレンスはスカラ値です。ですから配列の要素として配列のリファレンスを持つことができます。配列のリファレンスを作成するには「\」を使います。
my @person1 = ('Ken', 'Japan', 19); my @person2 = ('Taro', 'USA', 45); my @persons = (\@person1, \@person2);
またもっとも簡略な記法として外側の配列もリファレンスにしてしまって、次のように記述するのが一般的です。
my $persons = [ ['Ken', 'Japan', 19], ['Taro', 'USA', 45] ];
上記の記法がPerlでの二次元配列のもっとも一般的な表現になります。
配列の配列の要素の参照
作成した配列の配列の要素にアクセスしてみましょう。これは配列のリファレンスを要素として持つ配列のリファレンスですので、以下のようにアクセスすることができます。
my $name1 = $persons->[0]->[0]; my $country1 = $persons->[0]->[1]; my $age1 = $persons->[0]->[2]; my $name2 = $persons->[1]->[0]; my $country2 = $persons->[1]->[1]; my $age2 = $persons->[1]->[2];
矢印演算子を2回使うところがポイントです。Perlではふたつ目以降の矢印演算子を省略することができますので、次のように書くこともできます。
my $name1 = $persons->[0][0]; my $country1 = $persons->[0][1]; my $age1 = $persons->[0][2]; my $name2 = $persons->[1][0]; my $country2 = $persons->[1][1]; my $age2 = $persons->[1][2];
二次元配列のループ処理
二次元配列をループさせてみましょう。
すべての要素の出力
まずすべての要素を出力してみます。
for my $person (@$persons) { for my $column (@$person) { print "$column\n"; } }
次のように出力されます。
Ken Japan 19 Taro USA 45
Perlの二次元配列は実質としては、配列のリファレンスを要素に持つ配列のリファレンスです。ですからループさせる場合はデリファレンスして配列を取り出す必要があります。外側のループでは「@$persons」のように、内側のループでは「@$person」のようにデリファレンスを行う必要があるということです。
すべてのレコードをカンマ区切りで出力
次に以下のようにすべてのレコードをカンマ区切りで出力してみましょう。
Ken,Japan,19 Taro,USA,45
指定した文字列でレコードを連結するのにjoin関数を利用することができます。内側のループ処理は記述する必要はありません。
for my $person (@$persons) { print join(',', @$person) . "\n"; }
カンマ区切りのファイルから二次元配列を作成
今度は反対にカンマ区切りのファイルから二次元配列を作成することを考えてみましょう。以下のようなファイルがあるとします。ファイル名は「persons.txt」だとします。
Ken,Japan,19 Taro,USA,45
ファイルを読み込むにはopen関数を使用しますが、今回は読み込みを簡略にするために次のように単独のダイヤモンド演算子を使用してみましょう。while文と組み合わせて、引数で受け取ったファイルの各行を読み込むことができます。
while (my $line = <>) { ... }
実行するときはコマンドライン引数にファイル名を渡します。
perl script.pl persons.txt
ではカンマ区切りのファイルからから二次元配列を作成してみます。
my $persons = []; while (my $line = <>) { chomp $line; my @person = split(',', $line); push @$persons, \@person; }
chompは改行を取り除く関数です。指定した区切り文字で文字列を分解して配列を作成するにはsplit関数を使用します。push関数の第一引数は配列であることに注意しましょう。
「@$persons」のようにデリファレンスして渡す必要があります。またpushで追加する要素については、配列のリファレンスである必要があります。「\@person」のように配列のリファレンスを生成してpush関数の第二引数に指定します。
作成された二次元配列は以下のようになります。
my $persons = [ ['Ken', 'Japan', '19'], ['Taro', 'USA', '45'] ];
カンマ区切りよりはタブ区切りを使うほうがよい
今回はカンマ区切りで出力しましたが、業務で使う場合はタブ区切りで連結しておいたほうが使いやすいでしょう。カンマ区切りであれば文字の中にカンマを含まれている場合について意識しないといけないからです、タブであれば文字列の中にタブが含まれていることは少ないです。またタブ区切りにしておけば、Excelにそのまま貼り付けることができます。
print join("\t", @$person) . "\n";
ハッシュのリファレンス
ハッシュのリファレンスについて解説していきます。
ハッシュ
ハッシュの作成です。
my %person = (name => 'Ken', age => 19);
ハッシュのリファレンスの作成
ハッシュのリファレンスの作成です。リファレンスは「\」記号を使って作成します。
my %person = (name => 'Ken', age => 19); my $person = \%person;
「リファレンス」とは「指すもの」を意味します。$personは%personを指しています。
$person ---> %person
無名ハッシュのリファレンスの作成
無名ハッシュのリファレンスの作成です。「{}」は無名ハッシュ生成子と呼ばれます。
my $person = {name => 'Ken', age => 19};
$personは、名前を持たない「(name => 'Ken', age => 19)」というハッシュを指し示しています。
$person ---> (name => 'Ken', age => 19)
無名ハッシュのリファレンスの記述は、ハッシュのリファレンスを簡単に作成するための記述だと考えるとわかりやすいと思います。無名ハッシュ生成子を使うとハッシュのリファレンスを作成するために、ハッシュを作成する必要がなくなります。
# ハッシュのリファレンスの作成 my %person = (name => 'Ken', age => 19); my $person = \%person; # 上記の簡略表記 my $person = {name => 'Ken', age => 19};
Perlではハッシュとハッシュのリファレンスの両方の表記が出てくることがコードが読みにくいひとつの原因です。「ハッシュ」と「ハッシュのリファレンス」をきっちり区別してコードを読むことが大切です。
ハッシュのリファレンスのデリファレンス
ハッシュのリファレンスからハッシュを取り出すにはデリファレンスという操作を行う必要があります。ハッシュのデリファレンスには「%{}」を使用します。
my %person = %{$person};
次のように括弧を省略することもできます。
my %person = %$person;
ハッシュの要素を取得するにはデリファレンスしてから取得することもできますが、その方法は少し不便です。Perlではハッシュのリファレンスから要素を取得する方法が用意されています。ハッシュのリファレンスから要素を取り出すには、矢印演算子「->」を使用します。
my $name = $person->{name}; my $age = $person->{age};
以下のようにハッシュの要素を取得する場合と比較してみてください。矢印演算子があるかないかが違いになります。
my $name = $person{name}; my $age = $person{age};
Perlでプログミングする場合は、それがハッシュなのかハッシュのリファレンスなのかを常に意識してください。
ハッシュのリファレンスについては、以下のページで個別に詳しく解説しています。
ハッシュの配列
Perlでハッシュの配列を作成してみましょう。配列の配列のところで解説したように、配列の要素として持つことができるのはスカラ値のみです。ですから、配列の要素としてハッシュのリファレンスを指定する必要があります。
my %person1 = (name => 'Ken', country => 'Japan', age => 19); my %person2 = (name => 'Taro', country => 'USA', age => 45); my @persons = (\%person1, \%person2);
またもっとも簡略な記法として外側の配列もリファレンスにしてしまって、次のように記述するのが一般的です。
my $persons = [ {name => 'Ken', country => 'Japan', age => 19}, {name => 'Taro', country => 'USA', age => 45} ];
上記の記法がPerlでのハッシュの配列のもっとも一般的な表現になります。
ハッシュの配列をループさせてみましょう。
すべての要素の出力
まずすべての要素を出力してみます。
for my $person (@$persons) { for my $key (keys %$person) { my $value = $person->{$key}; print "$key : $value\n"; } }
次のように出力されると思います。
country : Japan name : Ken age : 19 country : USA name : Taro age : 45
Perlのハッシュの配列は実質としては、「ハッシュのリファレンス」を要素に持つ「配列のリファレンス」です。外側のループでは「@$persons」のように配列のデリファレンスを行います。内側のループではハッシュのキーをkeys関数を使って取得して、キーと対応するハッシュの値を出力しています。keys関数はハッシュのリファレンスではなく、ハッシュを受け取るので
keys %$person
のようにデリファレンスを行う必要があります。
ひとつの注意点としてはkeys関数で取得したキーの順序は不定であるということです。ですから、出力の順序を確定したい場合はsort関数を使ってキーを並べ替えます。
for my $person (@$persons) { for my $key (sort keys %$person) { my $value = $person->{$key}; print "$key : $value\n"; } }
すべてのレコードをカンマ区切りで出力
次に以下のようにすべてのレコードをカンマ区切りで出力してみましょう。
Ken,Japan,19 Taro,USA,45
レコードを指定した文字列で連結するのにjoin関数を利用することができます。出力する値を@recという配列に設定してからjoin関数で連結します。内側のループ処理は記述する必要はありません。
for my $person (@$persons) { my @rec = ( $person->{name}, $person->{country}, $person->{age} ); print join(',', @rec) . "\n"; }
カンマ区切りのファイルからハッシュの配列を作成
今度は反対にカンマ区切りのファイルからハッシュの配列を作成することを考えてみましょう。以下のようなファイルがあるとします。ファイル名は「persons.txt」だとします。
Ken,Japan,19 Taro,USA,45
ではカンマ区切りのファイルからからハッシュの配列を作成してみます。ファイルの読込については配列の配列のところで解説したように、単独のダイヤモンド演算子を利用します。
my $persons = []; while (my $line = <>) { # (1) 改行を削除 chomp $line; # (2) カンマ区切りの文字列を配列にする my @rec = split(',', $line); # (3) ハッシュのリファレンスを作成 my $person = {}; $person->{name} = $rec[0]; $person->{country} = $rec[1]; $person->{age} = $rec[2]; # (4) 配列のリファレンスに追加 push @$persons, $person; }
(1)chompは改行を取り除く関数です。(2)指定した区切り文字で文字列を分解して配列を作成するにはsplit関数を使用します。(3)ハッシュのリファレンスを作成してキーと値を設定します。(4)配列のリファレンスに追加します。push関数の第一引数は配列を受け取るので「@$persons」のようにデリファレンスする必要があります。
作成されたハッシュの配列は以下のようになります。
my $persons = [ {name => 'Ken', country => 'Japan', age => '19'}, {name => 'Taro', country => 'USA', age => '45'} ];
配列のハッシュ
次は「配列のハッシュ」について解説します。
配列のハッシュの作成
配列のハッシュの作成です。時間ごとの集計を行っている場面を想定してみましょう。
# 時間 => [件数, 平均応答時間, 最大応答時間] my $infos = { '01:01' => [3, 2.1, 4.6], '01:02' => [5, 4.1, 7.4], '01:03' => [6, 3.5, 5.7] };
1時1分には3件のアクセスがあり、平均応答時間は2.1秒、最大応答時間は4.6秒でした。1時2分には5件のアクセスがあり、平均応答時間は4.1秒、最大応答時間は7.4秒でした。1時3分には6件のアクセスがあり、平均応答時間は3.5秒、最大応答時間は5.7秒でした。このようなデータです。
すべての要素を出力する
すべての要素を出力してみましょう。
# (1)外側のループ for my $time (sort keys %$infos) { print "$time\n"; # (2) 内側のループ for my $column (@{$infos->{$time}}) { print "$column\n"; } }
(1)配列のハッシュとは配列を要素にもつハッシュのことですから、外側のループではハッシュを処理します。keys関数を使って時刻を取り出しています。
sort keys %$infos
のようにsortを行っているのは、時刻の順番で出力したいからです。
(2)内側のループは配列です。内側の配列は
@{$infos->{$time}}
のように取り出すことができます。
出力結果は以下のようになります。
01:01 3 2.1 4.6 01:02 5 4.1 7.4 01:03 6 3.5 5.7
すべてのレコードをカンマ区切りで出力
次に以下のようにすべてのレコードをカンマ区切りで出力してみましょう。
01:01,3,2.1,4.6 01:02,5,4.1,7.4 01:03,6,3.5,5.7
for my $time (sort keys %$infos) { my @rec = ($time, @{$infos->{$time}}); print join(',', @rec) . "\n"; }
「配列の配列」や「ハッシュの配列」のところで解説したように、join関数で連結するだけです。
カンマ区切りのファイルから配列のハッシュを作成
今度はカンマ区切りのファイルから配列のハッシュを作成することを考えてみましょう。以下のようなファイルがあるとします。ファイル名は「access.log」だとします。一つ目の列はアクセスのあった時刻(時:分)、ふたつめの列は応答時間です。
01:01,3 01:01,2 01:02,5 01:02,3 01:02,2 01:03,9 01:03,4 01:03,6 01:03,1
このようなファイルから、「時刻ごとの件数」と「応答時間の合計値」と「最大応答時間」をデータにもつ配列のハッシュを作成してみます。応答時間の合計値を保存しておくのは、後で「応答時間の合計/件数」で平均応答時間を求めることができるからです。
my $infos = {}; while (my $line = <>) { chomp $line; # (1)時刻と応答時間の取得 my ($time, $res_time) = split(',', $line); # (2)各時刻のデータを保存するための配列のリファレンス $infos->{$time} ||= []; # (3)件数の合計 $infos->{$time}[0]++; # (4)応答時間の合計 $infos->{$time}[1] += $res_time; # (5)最大応答時間 $infos->{$time}[2] = $res_time if !defined $infos->{$time}[2] || $res_time > $infos->{$time}[2]; }
(1)各行はカンマ区切りなのでsplit関数で時刻と応答時間を取得しています。(2)配列のリファレンスの中に時刻ごとの情報を持つので、まだ空の配列のリファレンスで初期化されていない場合に、初期化を行います。(3)件数を加算します。(4)配列の一つ目の要素に応答時間を合計します。(5)配列のふたつめの要素に最大応答時間を代入します。まだ応答時間が入っていないか、取得した応答時間が、過去の最大の応答時間を越えていた場合に代入します。
以下のような配列のハッシュできます。
my $infos = { '01:03' => [ 4, 20, 9 ], '01:01' => [ 2, 5, 3 ], '01:02' => [ 3, 10, 5 ] };
ハッシュのハッシュ
次は「ハッシュのハッシュ」について解説します。
ハッシュのハッシュの作成
ハッシュのハッシュの作成です。時間ごとの集計を行うときに行っている場面を想定してみましょう。
my $infos = { '01:01' => {count => 3, ave_time => 2.1, max_time => 4.6}, '01:02' => {count => 5, ave_time => 4.1, max_time => 7.4}, '01:03' => {count => 6, ave_time => 3.5, max_time => 5.7} };
1時1分には3件のアクセスがあり、平均応答時間は2.1秒、最大応答時間は4.6秒でした。1時2分には5件のアクセスがあり、平均応答時間は4.1秒、最大応答時間は7.4秒でした。1時3分には6件のアクセスがあり、平均応答時間は3.5秒、最大応答時間は5.7秒でした。このようなデータです。
すべての要素を出力する
すべての要素を出力してみましょう。
for my $time (sort keys %$infos) { print "$time\n"; # (2) 内側のループ for my $name (sort keys %{$infos->{$time}}) { my $value = $infos->{$time}{$name}; print "$name:$value\n"; } }
(1)ハッシュのハッシュとはハッシュを要素にもつハッシュのことですから、外側のループではハッシュを処理します。keys関数を使って時刻を取り出しています。
(2)内側のループはハッシュです。内側のループでも
keys %{$infos->{$time}}
のようにハッシュのキーを取り出しています。
出力結果は以下のようになります。
01:01 ave_time:2.1 count:3 max_time:4.6 01:02 ave_time:4.1 count:5 max_time:7.4 01:03 ave_time:3.5 count:6 max_time:5.7
すべてのレコードをカンマ区切りで出力
次に上記で作成したデータを以下のようにすべてのレコードをカンマ区切りで出力してみましょう。
01:01,3,2.1,4.6 01:02,5,4.1,7.4 01:03,6,3.5,5.7
for my $time (sort keys %$infos) { my @rec = ( $time, $infos->{$time}{count}, $infos->{$time}{ave_time}, $infos->{$time}{max_time} ); print join(',', @rec) . "\n"; }
「配列の配列」や「ハッシュの配列」のところで解説したように、join関数で連結するだけです。
カンマ区切りのファイルからハッシュのハッシュを作成
今度はカンマ区切りのファイルからハッシュのハッシュを作成することを考えてみましょう。以下のようなファイルがあるとします。ファイル名は「access.log」だとします。一つ目の列はアクセスのあった時刻(時:分)、ふたつめの列は応答時間です。
01:01,3 01:01,2 01:02,5 01:02,3 01:02,2 01:03,9 01:03,4 01:03,6 01:03,1
このようなファイルから、時刻ごとの件数と応答時間の合計値と最大応答時間をデータにもつハッシュのハッシュを作成してみます。応答時間の合計値を保存しておくのは、「応答時間の合計/件数」で平均応答時間を求めることができるからです。
my $infos = {}; while (my $line = <>) { chomp $line; # (1)時刻と応答時間の取得 my ($time, $res_time) = split(',', $line); # (2)各時刻のデータを保存するための配列のリファレンス $infos->{$time} ||= {}; # (3)件数の合計 $infos->{$time}{count}++; # (4)応答時間の合計 $infos->{$time}{total_time} += $res_time; # (5)最大応答時間 $infos->{$time}{max_time} = $res_time if !defined $infos->{$time}{max_time} || $res_time > $infos->{$time}{max_time}; }
(1)各行はカンマ区切りなのでsplit関数で時刻と応答時間を取得しています。(2)ハッシュのリファレンスの中に時刻ごとの情報を持つので、まだ空のハッシのリファレンスで初期化されていない場合に、初期化を行います。(3)件数を加算しています。(4)応答時間を合計しています。(5)最大応答時間を代入します。まだ応答時間が入っていないか、取得した応答時間が、過去の最大の応答時間を越えていた場合に代入します。
以下のようなハッシュのハッシュできます。
my $infos = { '01:03' => { 'max_time' => 9, 'count' => 4, 'total_time' => 20 }, '01:01' => { 'max_time' => 3, 'count' => 2, 'total_time' => 5 }, '01:02' => { 'max_time' => 5, 'count' => 3, 'total_time' => 10 } };
その他のリファレンス
終わりに
これで配列とハッシュを自由に扱うことができるようになりました。データを自由に扱えるようになるとプログラミングでできることがとても多くなります。また応用としてこれらのデータを自由に並べ替えることができるようにチャレンジしてみるのもよいでしょう。sort関数の解説が参考になると思います。