前回は、数値、文字列、変数、条件分岐の if 文と 繰り返しの while 文など、Perl の基本的なことを説明しました。今回も基礎知識の続きで、配列と連想配列について説明します。
Perl には複数個のスカラーを格納するデータとして、「配列」と「連想配列」があります。配列はC言語や Java でもお馴染みのデータですね。C言語や Java の場合、配列に格納するデータ型や大きさをあらかじめ宣言しなければいけませんが、Perl では不要です。同じ配列に数値 (整数や浮動小数点数) や文字列を混在させることもできます。
配列はホテルやマンションの部屋にたとえるとわかりやすいと思います。ホテル全体を配列とすると、各部屋がデータを格納する変数と考えることができます。ホテルでは、ルームナンバーによって部屋を指定しますね。配列の場合も、整数値によってデータを格納する変数を指定することができます。この整数値を「添字 (ソエジ : subscripts)」といいます。
添字 0 1 2 3 4 5 6 7 8 9 ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐ 配列 │ │ │ │ │ │ │ │ │ │ │ └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘ 図 : 配列の構造
たとえば、10 個のデータを格納する配列を考えてみます。これは、平屋建てのホテルで、部屋が 10 室あると考えてください。この場合は上図に示すように、データを格納する変数が並んでいて、それぞれ 0 から 9 までの添字で指定することができます。Perl の場合、C言語や Java と同じく、添字は 0 から順番に数えます。ただし、Perl では 1 次元配列しか使えないので注意してください。
配列は変数名の先頭に @ をつけて表します。
my @array; # 配列 array 全体を表す
C言語や Java では、配列の大きさを宣言しなくてはいけなかったのですが、Perl では不要です。大きさは Perl が自動的に調整してくれます。大きさを自由に変えることができる配列を「可変長配列」といいます。配列の大きさを気にせずにプログラミングできるのは、Perl の長所といっていいでしょう。
配列の要素は角カッコ [ ] を使ってアクセスします。
$array[0] = 10; # array の 0 番目の要素に 10 を代入 my $n = $array[1]; # array の 1 番目の要素を取り出して $n にセット
[ ] を使うのはC言語や Java と同じですね。配列の要素はスカラーなので、$ をつけてアクセスします。ここで、@array と $array は別の変数として扱われることに注意してください。$array はスカラーを格納する変数で、$array[ ] は配列 @array の要素をアクセスすることを表しているのです。
なお、C言語や Java と違い、Perl は添字に負の整数を指定すると、配列の後ろから数えます。たとえば、$array[-1] は最後尾の要素になり、$array[-2] は後ろから 2 番目の要素になります。
配列が数値を使って要素を指定するのに対し、連想配列 [*1] は文字列を使って指定します。これはC言語にはない Perl の大きな特徴のひとつです。連想配列は変数名の先頭に % をつけて表します。
my %table; # 連想配列 table 全体を表す
$table{'abc'} = 1; # table の abc に 1 をセット my $n = $table{'def'}; # table の def から要素を取り出して $n にセット
連想配列は、配列と同様にスカラー型データ [*2] しか格納できません。要素にアクセスする場合、名前の先頭に $ をつけるのは配列と同じですが、[ ] のかわりに { } を使い、数字ではなく文字列で要素を指定します。
配列と連想配列はカッコ ( ) を使って要素をまとめて代入することができます。
my @array = (10, 20); # $array[0] = 10; $array[1] = 20; と同じ my %table = ('abc', 30, 'def', 40); # $table{'abc'} = 30; $table{'def'} = 40; my %table = (abc => 30, def => 40); # 同上
Perl では ( ) でデータを並べたものをリスト [*3] といいます。連想配列の初期値に演算子 => を使う場合、キー (文字列) の指定で ' や " を省略できるので便利です。
リストに変数を並べて、そこに値を代入することもできます。
my @array = (10, 20, 30); my ($a, $b, $c) = @array; # $a = $array[0]; # $b = $array[1]; # $c = $array[2]; と同じ
リストを使って複数の変数をまとめて宣言することができます。これはとても便利な書き方ですね。
このほかに、Perl ではリストの中に配列を置くこともできます。
my @array = (10, 20, 30); (1, 2, @array, 3, 4) => (1, 2, 10, 20, 30, 3, 4) と同じ
このように、配列の要素はリストの中に展開されるのです。もちろん、代入することもできます。
my ($a, $b, @c) = (10, 20, 30, 40, 50); => $a = 10, $b = 20, @c = (30, 40, 50) と同じ
この場合、$a と $b に値がセットされ、@c には残りの値がセットされます。リストを使うことで配列の分解・合成は簡単に行うことができます。
Perl では、配列変数の値を配列変数に代入することができます。この場合、配列の内容がコピーされます。次の例を見てください。
my @a = (10, 20, 30, 40, 50); my @b = @a; $a[0] = 100; # 配列 @a を書き換える print "@a\n"; # 100 20 30 40 50 と表示 print "@b\n"; # 10 20 30 40 50 と表示
配列 @a の内容を変更しても、配列 @b の内容は変わっていません。代入演算子 = で配列の内容をコピーすることは、C言語ではできません。
配列の一部分を取り出すことも簡単にできます。
my @b = @array[3,4,5]; # @b = ($array[3],$array[4],$array[5]) と同じ
これも便利な書き方ですね。ところで、この方法は文字列には使えません。C言語の文字列は文字の 1 次元配列のことなので、[ ] を使ってアクセスすることができましたが、Perl の文字列は別のデータ型として定義されているので、配列のようにアクセスすることはできません。部分文字列を取り出すには関数 substr を使います。文字列操作関数は、正規表現と一緒に次回で詳しく説明します。
それでは、配列をスカラー変数に代入することはできるのでしょうか。ほかのプログラミング言語を知っている方であれば、エラーになると思われることでしょう。ところが Perl では可能なのです。
my @array = (10, 20, 30, 40, 50); my $a = @array; # $a => 5
Perl では、スカラーが必要な処理で配列が与えられると、配列はスカラーに変換されます。その逆の場合もあります。この場合、配列 @array はスカラー変数 $a に代入されるので、スカラーに変換されたのです。配列全体を表す @array は、スカラーに変換されると配列に格納されている要素数を表します。また、$#array という特別な変数があり、これは配列 array の最後の要素の添字 (インデックス) になります。
数値と文字列の場合もそうでしたが、Perl はプログラムを実行する状況に応じて、データを自動変換します。つまり、「スカラー変数への代入」や「数値の足し算」といったデータの操作から、必要となるデータの種類がわかるので、その条件を満たすようにデータの自動変換が行われるのです。
スカラーが要求される状況を「スカラーコンテキスト」といい、配列が要求される状況を「リストコンテキスト」といいます。コンテキスト (context) は文脈、背景、環境という意味です。
Perl にはコンテキストの違いによって、返す値が異なる処理がたくさんあります。@array は、リストコンテキストでは要素をリストに展開してから相手に渡し、スカラーコンテキストでは配列の要素数を返すのです。Perl では複数個のデータを受け渡す場合、一時的なリストを作成して行います。
実は、行入力演算子もそうなのです。スカラーコンテキストではファイルから 1 行入力して返しますが、リストコンテキストでは凄いことを行います。
open my $in, "<", "filename"; my @data = <$in>; close $in;
この処理を実行すると、ファイル filename の内容すべてを配列 @data に読み込むことができます。行入力演算子は、リストコンテキストで動作する場合、ファイルの内容を全部読み込み、行単位で分解してリストにまとめて返すのです。
このように、コンテキストの違いによって動作が異なることは、使い方によってはたいへん便利なのですが、初心者にとっては苦労するところです。初めのうちはコンテキストの違いもよくわからないし、思ったようにプログラムが動作しないこともあるでしょう。良くも悪くも、これが Perl の特徴なのです。まあ、Perl に慣れてくると自然と理解できることなので、どんどんプログラムを作っていきましょう。
配列を操作するときに、とても役に立つ繰り返し文を紹介しましょう。まず、C言語や Java でもお馴染みの for 文です。Perl の for 文はC言語とほぼ同じです。
for (初期化; 条件部; 更新処理) { 処理A; ...; 処理Z; } ↓ ┌─────┐ │ 初期化 │ └─────┘ ├←─────┐ FALSE ┌─────┐ │ ┌───│ 条件部 │ │ │ └─────┘ │ │ ↓TRUE │ │ ┌─────┐ │ │ │ 処理A │ │ │ └─────┘ │ │ ・ │ │ ・ │ │ ┌─────┐ │ │ │ 処理Z │ │ │ └─────┘ │ │ ↓ │ │ ┌─────┐ │ │ │ 更新処理 │ │ │ └─────┘ │ │ └──────┘ └──────┐ ↓ 図 : for 文の処理
for 文の特徴は、いちばん最初に行われる初期化と、繰り返すたびに行われる更新処理があることです。上図を見ればおわかりのように、初期化はただ一度しか行われず、更新処理はブロックの処理を実行してから行われます。簡単な使用例を示しましょう。
リスト : for 文の使用例 (test01.pl) use strict; use warnings; for (my $i = 0; $i < 8; $i++) { print "$i\n"; }
$ perl test01.pl 0 1 2 3 4 5 6 7
まず、my $i = 0; で変数 $i を 0 に初期化します。この処理は for が始まるときに一度だけ実行されます。次に条件部 $i < 8 がチェックされます。$i は 0 ですから条件を満たしますね。そこで、ブロックの処理が行われ、print により $i の値が表示されます。
次に、更新処理 $i++ が行われます。++ はインクリメント演算子といって変数の値を +1 します。つまり、$i = $i + 1; と同じ処理です。C言語ではお馴染みの処理ですね。実は ++$i のように、++ を変数の前につけることもできます。前と後ろでは +1 することは変わりませんが、処理結果が異なるところがあります。これは、それが問題になるところで説明しましょう。もちろん、値を -1 するデクリメント演算子 -- も使えます。
変数 $i の値を +1 したら、条件部のチェックを行います。あとは、while と同様に条件部が成立しているあいだは、ブロックの処理と更新処理を繰り返します。結局、$i の値は 8 になるので、条件部が不成立となり繰り返しを終了します。したがって、このプログラムを実行すると 0 から 7 までの数字が表示されます。
次は、C言語にはない繰り返し文を紹介しましょう。foreach 文 [*4] は配列から順番に要素を取り出して変数に代入し、ブロックに書かれている処理を繰り返します。
foreach my 変数 (@array) { 処理A; ...; 処理Z; } ↓ ├←────┐ No ┌─────┐ │ ┌───│要素がある│ │ │ └─────┘ │ │ ↓Yes │ │ ┌───────┐ │ │ │$x ← 次の要素│ │ │ └───────┘ │ │ ↓ │ │ ┌─────┐ │ │ │ print $x;│ │ │ └─────┘ │ │ └─────┘ └──────┐ ↓ 図 : foreach 文の処理
変数が省略されると、配列の要素は特殊変数 $_ に代入されます。たとえば、配列 @array の要素の合計を求める場合は、次のようにプログラミングできます。
リスト : 合計を求める use strict; use warnings; my @array = (1, 2, 3, 4, 5); my $sum = 0; foreach my $i (@array) { $sum += $i; } print "$sum\n";
for 文を使ってプログラミングすると、次のようになります。
リスト : 合計を求める (2) use strict; use warnings; my @array = (1, 2, 3, 4, 5); my $sum = 0; for (my $i = 0; $i < @array; $i++) { $sum += $array[$i]; } print "$sum\n";
条件部の比較演算子 < はスカラーコンテキストですから、@array は配列の要素数を表しています。foreach 文の方が for 文よりも簡単ですね。
for 文、foreach 文は while 文と同様に、last によって繰り返しを脱出することができます。このほかに、Perl の繰り返し文では次に示す制御を行うことができます。
last 繰り返しの終了 (break と同じ) next 更新処理へジャンプ (continue と同じ) redo 繰り返しの先頭へジャンプ (条件部、更新処理は実行しない)
last は簡単ですが、next と redo は説明が必要ですね。次の図を見てください。
↓ ┌─────┐ │ 初期化 │ └─────┘ ├←─────┐ FALSE ┌─────┐ │ ┌───│ 条件部 │ │ │ └─────┘ │ │ ┌───→↓TRUE │ │ │ ・ │ │ │ ┌────┐ │ │ │ │ next │──┐ │ │ │ └────┘ │ │ │ │ ・ │ │ │ │ ┌────┐ │ │ │ └─ │ redo │ │ │ │ └────┘ │ │ │ ・ │ │ │ ↓ ←───┘ │ │ ┌─────┐ │ │ │ 更新処理 │ │ │ └─────┘ │ │ └──────┘ └──────┐ ↓ 図 : next と redo の処理
next はC言語や Java の continue と同じ働きをします。next が実行されると、残りの処理を中断してブロックが終了したところへジャンプします。for 文では、このあとに更新処理が行われます。while 文は更新処理がないので、条件部のチェックが行われます。
これに対し、redo は繰り返しの先頭に戻るだけで、for であれば更新処理、while であれば条件部のチェックを実行しません。redo はC言語や Java ではサポートしていない Perl 独自の機能です。
次は、for 文や foreach 文を使うときに便利な「範囲演算子」を説明します。範囲演算子はリストコンテキストとスカラーコンテキストで動作が異なりますが、今回はリストコンテキストの動作を説明します。
s .. e => s から e までを 1 刻みで並べたリストを返す
簡単な例を示します。範囲演算子を使うと、hello, world を 10 回表示するプログラムは次のようになります。
リスト : hello, world の表示 foreach (1 .. 10) { print "hello, world\n"; }
while 文や for 文を使うよりも簡単です。もう一つ簡単な例を示しましょう。1 から 1000 までを加算するプログラムは次のようになります。
リスト : 1 から 1000 までの合計値 my $sum = 0 foreach (1 .. 1000) { $sum += $_; } print "$sum\n";
変数 $i には 1 から 1000 までの数が順番にセットされ、変数 $sum に加算されます。答えは 500500 になります。
また、配列から部分列を取り出すときにも範囲演算子を使うことができます。
my @a = (1 .. 100); # 1 から 100 までを格納した配列を生成 my @b = @a[10 .. 19]; # 10 番目から 19 番目までの要素を取り出す my @c = @a[-10 .. -1]; # 末尾から 10 個の要素を取り出す
配列は角カッコ [ ] を使ってアクセスするだけではありません。Perl には便利な関数が用意されています。
push @a, $x; 配列の最後尾にデータを追加する pop @a; 配列の最後尾からデータを取り出す shift @a; 配列の先頭からデータを取り出す unshift @a, $x; 配列の先頭にデータを追加する
push, pop は配列の後方から、shift, unshift は配列の前方からデータを操作します。push, unshift は、挿入したあとの配列の要素数を返します。まず、push, pop の動作を確認してみましょう。
リスト : push, pop の動作 (test02.pl) use strict; use warnings; my @array = (); for (my $i = 0; $i < 5; $i++) { push @array, $i; print "[@array]\n"; } while (@array) { my $i = pop @array; print "$i [@array]\n"; }
まず配列 @array を空にします。まず、push で @array にデータをセットし、配列にデータがあるあいだ、pop で取り出します。実行結果は次のようになります。
$ perl test02.pl [0] [0 1] [0 1 2] [0 1 2 3] [0 1 2 3 4] 4 [0 1 2 3] 3 [0 1 2] 2 [0 1] 1 [0] 0 []
push で配列の後ろにデータが追加されていき、pop によって配列の後ろからデータが取り出されていくことがわかります。
次は、shift, unshift の動作を確認します。
リスト : shift, unshift の動作 (test03.pl) use strict; use warnings; my @array = (); for (my $i = 0; $i < 5; $i++) { unshift @array, $i; print "[@array]\n"; } while (@array) { my $i = shift @array; print "$i [@array]\n"; }
push を unshift に、pop を shift に置き換えただけです。 実行結果は次のようになります。
$ perl test03.pl [0] [1 0] [2 1 0] [3 2 1 0] [4 3 2 1 0] 4 [3 2 1 0] 3 [2 1 0] 2 [1 0] 1 [0] 0 []
push とは逆に、unshift によって配列の前からデータが挿入され、要素がひとつずつ右へ移動 (shift) していることがわかります。shift の場合も、配列の前からデータが取り出されるので、要素がひとつずつ左へ移動しています。
Perl の場合、特殊変数 @ARGV にコマンドラインで与えられた引数が格納されています。次の例を見てください。
リスト : コマンドライン引数 (test04.pl) use strict; use warnings; print "[@ARGV]\n";
test04.pl は配列 @ARGV の内容を表示するだけです。3 つの引数を与えて起動すると、結果は次のようになりました。
$ perl test04.pl foo bar baz [foo bar baz]
test04.pl は @ARGV に含まれていないことに注意してください。test04.pl は特殊変数 $0 に格納されます。また、pop と shift は、引数が省略されると @ARGV を操作します。$_ ではないので注意してください。
file1 + file2 標準出力 --------------------------------- abcd ABCD abcdABCD efgh + EFGH ===> efghEFGH ijkl IJKL ijklIJKL 図 : 行単位の連結
最後に簡単な例題として、2 個のファイルを連結するプログラムを作ってみましょう。ただし、2 個のファイルの同じ行を連結をします。上図に示すように、2 個のファイル file1 と file2 の各行を連結して、標準出力へ書き出します。このプログラムのポイントは、入力用のファイルハンドルを 2 個用意することです。
┌──────┐ ┌─────┐ │ Perl │──────────│ │ │ハンドル IN1│←←←←←←←←←←│ファイルA│ │ │ │──────────│ │ │ │ │ └─────┘ │ │ │ ┌─────┐ │ │ │──────────│ │ │ハンドル IN2│←←←←←←←←←←│ファイルB│ │ │ │──────────│ │ │ │ │ └─────┘ │ │ │ ┌────┐ │ │ │──────────│ │ │ └──→│→→→→→→→→→→│ 画面 │【標準出力】 │ │──────────│ │ └──────┘ └────┘ 図 : 2 個のファイルをオープンする
上図に示すように、2 個のファイルを同時にオープンして、入力用のファイルハンドル IN1 と IN2 を用意します。IN1 に行入力演算子を適用すれば、ファイルAから 1 行分データをリードすることができます。同様に、IN2 に行入力演算子を適用すれば、ファイルBからデータをリードできるのです。あとは、文字列を連結して標準出力へ書き出せばいいわけです。
ひとつだけ注意点があります。それは、2 個のファイルの行数は同じとは限らない、ということです。つまり、どちらかのファイルが先に終了する場合があるのです。この場合は、残ったファイルをそのまま出力します。これを図に示すと、次のようになります。
↓ ┌────────────┐ │ファイルA,Bをオープン│ └────────────┘ ├←────┐ ↓ │ ┌───────┐ EOF┌────────┐ │ ┌←─│ファイルB出力│←─│ファイルAリード│ │ │ └───────┘ └────────┘ │ │ ↓ │ │ ┌───────┐ EOF┌────────┐ │ │ │ バッファ出力 │←─│ファイルBリード│ │ │ └───────┘ └────────┘ │ │ ↓ ↓ │ │ ┌───────┐ ┌────────┐ │ ├←─│ファイルA出力│ │行を連結して出力│ │ │ └───────┘ └────────┘ │ │ ↓ │ ↓ └─────┘ └──────────────┐ ↓ 図 : 処理の流れ
ファイルAが終了した場合は、ファイルBをそのまま出力すればいいのですが、ファイルBが終了した場合は、ファイルAからリードしたデータがまだ残っているので、それを出力してからファイルBをすべて出力します。
プログラムは次のようになります。流れ図とは少し異なるので注意してください。
リスト : 行単位の連結 (paste.pl) use strict; use warnings; open my $in1, "<", shift or die "Can't open file.\n"; # open my $in1, "<", $ARGV[0]; open my $in2, "<", shift or die "Can't open file.\n"; # open my $in2, "<", $ARGV[1]; while (<$in1>) { my $line2 = <$in2>; unless ($line2) { print; last; # ループから脱出 } chop; # 改行を取り除く print $_ . $line2; } # 残りのファイルを出力 if ($_) { while (<$in1>){ print; } } else { while (<$in2>){ print; } } close $in1; close $in2;
最初にファイル名を @ARGV から取得し、ファイルハンドルを生成します。shift を使って @ARGV からデータを取り出しています。コメントに書いてある方法でもかまいません。次の while 文で $in1 からデータを入力します。最初に、$in2 から1行入力して $line2 にセットします。<$in2> は while の条件部で使われていないので、データは $_ にはセットされません。
次の unless 文で、$in2 からのデータを確認します。unless は if と逆で、「もしも~~でなければ○○をせよ」という条件分岐を行います。つまり、条件が成立しない場合にブロックを実行します。C言語は unless をサポートしていません。この場合、if 文と否定演算子を組み合わせて実現しなければいけません。unless を使うと素直にプログラムできるので、とても便利です。ただし、unless は if とは違って、else ブロックは使えないので注意してください。
ファイルが終了した場合、行入力演算子は「偽」を返すので、変数 $line2 をチェックすればいいわけです。この場合は、$in1 のデータである $_ を出力してから last で while ループを抜けます。$line2 にデータがある場合は文字列を連結して出力します。
行入力演算子は、入力データから改行文字 \n を削除していません。行単位で連結するのですから、$_ の改行文字は邪魔になります。そこで、文字列の最後の文字を切り落とす関数 chop を使って、改行文字を切り落としています。chop は引数が与えられていない場合は $_ を操作対象とします。Perlでは、改行文字を取り除く処理で chop をよく使います。あとは 2 つの文字列を演算子 . で連結して出力します。
while ループを終了したら、残りのファイルを出力します。$_ にデータがセットされているときは、ファイルハンドル $in2 が終了した場合なので、$in1 を最後まで出力します。逆に、$_ にデータがセットされていない場合は、ファイルハンドル $in1 が終了したので、$in2 を最後まで出力すればいいのです。
これでプログラムは完成です。興味のある方は実際に試してみてください。