┌──────────────────┐
│Perl入門 ~構造化プログラミング編~ │
│(6)エラー処理 │
│2008/05/25 │
└──────────────────┘
[▲Perl Home▲]
●例外処理(1):dieとevalを使う
◆エラー処理の必要性
・ツールの有用性が高くなると、自分だけでなく多くの人が、様々な用途で使用す
るようになってきます。すると当然想定外の要素も多くなり、それ故のエラーも
増えてきます。
・エラー処理とは、想定外の事柄...つまり「例外」をどう処理するかというもの
です。そこで深刻なエラーの処理について話をしたいと思います。
◆evalによる例外補足
・典型的なシステムエラー、例えば「0で割ってしまった」というケースを考えて
みましょう。下記のようなコードでは当然システムエラーが起きます。
┌───────────────────────────────────┐
#!/usr/local/bin/perl
use strict;
{
my $op_a;
my $op_b;
my $ans;
$op_a=20, $op_b=0; # 分母が0!!
$ans = CalDiv($op_a, $op_b);
print "$op_a / $op_b = $ans\n";
}
sub CalDiv {
my ($a, $b) = @_;
my $c;
$c = $a / $b;
return($c);
}
└───────────────────────────────────┘
┌───────────────────────────────────┐
C:>test_err.pl
Illegal division by zero at test_err.pl line 18.
└───────────────────────────────────┘
・このままだとプログラムがエラー終了するので、例えば
+ division by zero が発生したら、STDERRにメッセージを表示
+ 割り算の解には ERROR を入れておく
のようにしたいこともあるでしょう。こんなときには「eval」を使用します。
・evalは下記の機能を持ちます。
+ 実行時のエラーを補足
+ 特殊変数「$@」にエラーメッセージを入れる
→ エラー無い場合は、$@は空文字列になる
・この機能を実装したコードを示します。division by zeroで強制終了せずにプロ
グラムが続行されていることがわかると思います。
・特殊変数「$@」でエラー処理をしている部分で、関数warnを使用しています。こ
れはユーザ定義の文字列に加えて、該当部のファイル名と行番号も付加したうえ
でSTDERRに表示します。
┌───────────────────────────────────┐
#!/usr/local/bin/perl
use strict;
{
my $op_a;
my $op_b;
my $ans;
$op_a=20, $op_b=0; # 分母が0!!
$ans = CalDiv($op_a, $op_b);
print "$op_a / $op_b = $ans\n" if ($ans ne 'ERROR');
$op_a=20, $op_b=10; # 普通に計算
$ans = CalDiv($op_a, $op_b);
print "$op_a / $op_b = $ans\n" if ($ans ne 'ERROR');
}
sub CalDiv {
my ($a, $b) = @_;
my $c;
eval { $c = $a / $b; }; # 最後「;」に注意
if ($@) { # division by zeroの処理
$c = 'ERROR';
warn "$a / $b = ERROR";
}
return($c);
}
└───────────────────────────────────┘
┌───────────────────────────────────┐
C:>test_err.pl
20 / 0 = ERROR at test_err.pl line 25. # これは STDERR
20 / 10 = 2 # これは STDOUT
└───────────────────────────────────┘
◆die関数とeval
・しかし上記のエラーメッセージを見ると、25行目に問題あるように見えますが、
本当は「0で割算」命令を発行した「10行目」が悪者です。つまり、本来であれ
ばエラーを起こした起点に情報を伝播させる必要があります。
・このときは、die関数 と evalを組み合わせて使用します。die関数はwarn関数と
同様にSTDERRにメッセージを表示して、プログラムを停止させます。
・die関数のよく見る形としては
open(FH, "< read_file.txt") or die;
のような使い方をしていることが多いと思います。上記は「or演算」の左辺が
undefで終わった場合に、右辺実行でdieによるプログラム終了です。
・すると「あれ? エラー伝播(再投入)したいのに、終了関数die()を使うってどう
いうこと?」と感じるでしょう。
・そうです。ここがポイントです。今回の目的「エラーを起こした起点に情報を伝
播させる」では「evalブロックの中でdie関数を使う」のです。die関数の機能は
+ プログラム終了させる
+ 例外を投入する
ですが、このうち前者の「プログラム終了」をevalによって止める作戦です。
・evalブロックの中でdie関数を使うと、エラーメッセージを特殊変数$@に入れた
後も、dieによるプログラム終了が発生しません(eval自身で補足される)。その
まま呼び出し側に投入して、再度エラー補足させた際に「特殊変数の$@の中身を
使わせる」という形になります。
・ちょっとややこしいので、とにかく例を見ることにしましょう
┌───────────────────────────────────┐
#!/usr/local/bin/perl
use strict;
{
my $op_a;
my $op_b;
my $ans;
$op_a=20, $op_b=10; # 普通に計算
my $ans = CalDiv($op_a, $op_b) or die;
$op_a=20, $op_b=0; # 分母が0!!
my $ans = CalDiv($op_a, $op_b) or die; # このdieで再補足
}
sub CalDiv {
my ($a, $b) = @_;
my $c;
eval {
($c = $a / $b) or die; # eval内では、dieでも終了しない!!
}; # 最後「;」に注意
if ($@) {
warn "$a / $b = ERROR\n"; # \n終了で付加メッセージ無し
} else {
print "$a / $b = $c\n";
}
return($c); # ERROR時はundef戻し
}
└───────────────────────────────────┘
┌───────────────────────────────────┐
20 / 10 = 2
20 / 0 = ERROR
Illegal division by zero at test_err.pl line 21.
...propagated at test_err.pl line 13.
└───────────────────────────────────┘
・この例を見るとわかるように、21行目で発生したシステムエラーはevalブロック
内なので、die関数が実行されても終了しません。ただし、$@へのエラーメッセ
ージはセットされます。
・サブルーチン CalDiv は undef を戻すので、13行目で左辺不成立により再度die
関数が実行されています。このとき特殊変数$@の中身が空文字列ではないので、
die関数は
\t...propagated at
をメッセージとして付加します。
・この結果生成されたエラーメッセージを見ると、21行目でdivision by zeroが発
生し、それは13行目から伝播したものであることがわかります。
◆実はtry~catchで書くことも
・今回 eval/dieで記述した例外処理。C++等では、try/catchで書きますが、実は
Perlでもtry/catch記述を使用することができます。それにはCPANの
http://search.cpan.org/~uarun/Error-0.15/Error.pm
を導入する必要があります。
・上記moduleは、まだ「Perlをインストールすれば黙って入ってくる」ものではな
いので、今回は説明しませんが、C++での記述に慣れている方には良いかもしれ
ません。
●例外処理(2):Carpモジュールのcroak/carpを使う
◆トレース情報を得るならCarpを使うのが楽
・ここまでエラー情報のトレースを行わせるために、evalとdieを使いました。し
かし常にトレース情報を得たい場合、eval/dieよりも便利な方法があります。そ
れは
Carpモジュール
を使うことです。
・このPerl解説のコンテンツでは「構造化プログラミング」を意識していますが、
構造化コーディングすなわちサブルーチンを多用した場合や、コードがモジュー
ルとして利用されている場合、先ほどのようにeval/dieを使った方法では
+ 例外catch/throwの仕組みが可能なことはわかったが
+ 再投入/再補足がちょっと面倒
- eval {}クロージャ内のスコープを意識するとかも
が少し引っかかっていると思います。
・そこでエラートレースをするためであればCarpモジュールを使います。むしろ
Perl5.8以上ではCarpモジュールを使うべきだと思います。
・Carpモジュールでは、下記の関数が使用できます。(*1)
carp / cluck : warn関数相当
croak / confess : die関数相当
・warn/dieとの違いは、呼び出し元の情報:スタックのバックトレース情報を表示
する点にあります。
・これもサンプルコードを見てみましょう。下記のサンプルコードは引数のファイ
ルを全て読み込み/表示するものですが、引数指定が無い場合と、一部のファイ
ルが無い場合について実行してみました。
┌───────────────────────────────────┐
#!/usr/local/bin/perl
use Carp;
use strict;
{
my $src = ReadFiles();
PrintDatas($src);
}
#====================================================
sub ReadFiles {
ArgCheck() or croak;
my $src = AccumulateDatas();
return($src);
}
#====================================================
sub PrintDatas {
my ($src) = @_;
for (my $i=0; $i<@{$src}; $i++) {
print "==== FileName:",$src->[$i]->{name}," ====\n";
my $buf = $src->[$i]->{list};
for (my $j=0; $j<@{$buf}; $j++) {
print $buf->[$j];
}
}
}
#====================================================
sub AccumulateDatas {
my @src;
for (my $i=0; $i<@ARGV; $i++) {
open(FH, "< $ARGV[$i]")
or carp "WARN:$ARGV[$i]が無い";
my @sub_src = <FH>;
push(@src, {
'name' => $ARGV[$i],
'list' => \@sub_src } );
close(FH);
}
return(\@src);
}
#====================================================
sub ArgCheck {
($#ARGV >=0) or croak "ERROR:引数指定無い";
return($#ARGV);
}
└───────────────────────────────────┘
┌───────────────────────────────────┐
>test_err.pl # 引数無し実行
ERROR:引数指定無い at test_err.pl line 51
main::ArgCheck() called at test_err.pl line 13
main::ReadFiles() called at test_err.pl line 6
└───────────────────────────────────┘
┌───────────────────────────────────┐
>test_err.pl data0.txt data1.txt # data1.txt存在しない
WARN:data1.txtが無い at test_err.pl line 37
main::AccumulateDatas() called at test_err.pl line 14
main::ReadFiles() called at test_err.pl line 6
==== FileName:data0.txt ====
data0 00 matsudo
data0 01 kamihongo
data0 02 matsudoshinden
data0 03 minoridai
data0 04 yabashira
data0 05 tokiwadaira
data0 06 goko
data0 07 motoyama
data0 08 kunugiyama
data0 09 kitahatsutomi
==== FileName:data1.txt ====
└───────────────────────────────────┘
・実行結果を見ると、引数無しの場合はcroak関数が効いて実行止まりますが、そ
の際に呼び出し元へのトレース情報も出ていることがわかります。
・ファイル無しの場合は、carp関数が効いて実行は止まりませんが、このときも呼
び出し元へのトレース情報が出ています。
●まとめ
・これでPerl構造化コーディングに関する基礎的な説明は大体できました。
・今後はいろいろなModuleを使用/作成する本格的なコーディング又は、オブジェ
クト指向へ移ることになると思いますが、このレポートが一助になることを願っ
ております。
Monpe
(*1)今回のサンプルコードではcarp/cluck と croak/confessの表示に違いは出ません。
[▲Perl Home▲]
Copyright(c)2008 Monpe
All Rights Reserved