小池啓仁 ヒロヒト応援ブログ By はてな

小池啓仁(コイケヒロヒト)の動画など。

小池啓仁 ヒロヒト応援ブログ By はてな

Perlオブジェクト基礎文法最速マスター

Perl5でのオブジェクトの基礎を解説したいと思います。
Perlでのオブジェクト指向と言えば、モダンPerl系ではMooseモジュール等なのですが、今回は、あくまでも基礎編です。


Perlで最速にオブジェクトをマスターするには、まず、「リファレンス」と「use宣言」と「bless」の3つが理解できていないとなりません。


なぜかと言うと、Perlでのオブジェクトはblessされたリファレンスなのです。また、Perlでのクラスはモジュールなのです。そのモジュールでの一番の謎は、use宣言なのです。


なので、はじめにこの3つを理解してから、そのあと「オブジェクト」と「オブジェクト指向」について解説していきます。
つまり、以下の順番です。

  • リファレンスについて
  • use宣言について
  • blessについて
  • Perlでのオブジェクトについて
  • Perlでのオブジェクト指向について



リファレンスについて

普通、リファレンスといえば、一覧の機能参照のことですが、Perlでリファレンスといえば、言語仕様のアドレス参照のことなのです。
まぁ、PerlでのリファレンスをC言語でいえば、アドレス演算ができないポインタのようなものなのです。


たとえば、配列変数 @array のリファレンス(参照)は、 \演算子で取得することができます。
以下は、配列変数 @array のリファレンスをスカラー変数 $array_ref1 へ格納しています。

my @array = (1,2,3);
print $array[2], "\n"; # 3番目の要素を表示
my $array_ref1 = \@array; # 配列変数のリファレンスをスカラー変数へ格納


つぎにリファレンスから元の値を求めたい時は、デリファレンスすると言います。
デリファレンスには、参照先の変数の識別子を先頭に付け足す方法と矢印演算子『->』を使う方法の2通りがあります。

print $$array_ref1[2], "\n";  # 3番目の要素を表示;
print $array_ref1->[2], "\n"; # 3番目の要素を表示


リファレンスの利用場面は、私が思いつくところでは以下のケースです。

  • 多次元配列を処理するケース
  • 関数をリファレンスでコールするケース
  • 関数の引数へ配列を渡すケース
  • 関数のリターン値にするケース
◆多次元配列を処理するケース
my $array_ref1 = \@array; 

配列変数の@の前に、\をつけると、配列のリファレンスが取得できます。
上記は、配列@arrayのリファレンスを$array_ref1へ格納しています。
また、無名配列のリファレンスを格納することもできます。

my $array_ref2 = [ 1, 2, 3, 4 ]; 

リストを [ ] でくくると、無名配列になり、そのリファレンスを$array_ref2へ格納しています。


つぎに、リファレンスから元の値を取得することをデリファレンスといいます。

my $array_ref2 = [ 1, 2, 3, 4 ]; 
print $array_ref2->[3]; #デリファレンス

ちなみに、『->』は矢印演算子といい、リファレンスをデリファレンスします。


補足:リファレンスの元をリファレントいいます。
つまり、$array_ref1がリファレンスで、\@arrayがリファレントです。


この機能を利用すると多次元配列の処理が可能です。
以下は、配列変数 @a の要素に配列のリファレンスを各々格納していて、結果的に2次元配列にしています。

@a = ( [ qw(00 01 02) ],
       [ qw(10 11 12) ],
       [ qw(20 21 22) ]);
for $i (0 .. $#a) {
    for $j (0 .. $#{$a[$i]}) {
        print '$a[' . $i . '][' . $j . ']=' . $a[$i][$j] . '   ';
    }
    print "\n";
}

尚、上記では『->』矢印演算子がありませんが、大括弧(ブラケット)や中括弧(ブレース)の間にはさまれた矢印は省略可能なのです。

◆関数をリファレンスでコールするケース

関数のリファレンスも配列と同様にできます。

sub01("aaa");   # 普通にコール
$sub = \&sub01;
$sub->("aaa");  # リファレンスでコール
sub sub01 {
    print "--- sub01 $_[0]---\n";
}

『$sub = \&sub01;』で関数のリファレンスを格納して、『$sub->("aaa")』でコールしています。

◆関数の引数へ配列を渡すケース

Perlでの関数の引数はリストです。
引数に配列変数を渡すとリストに展開されてしまい、普通は上手く配列変数を渡せません。
そこで登場してくるのが配列変数のリファレンスです。
リファレンスなら、上手く配列変数を渡せるのです。

my @bbb = ("bbb", "ccc", "ddd");
my @xxx = ("xxx", "yyy", "zzz");
sub02("aaa", \@bbb, \@xxx);  
sub sub02 {
    print "--- sub02 $_[0]---\n";
    print "--- sub02 $_[1]->[0]---\n";
    print "--- sub02 ${$_[1]}[1]---\n";
    print "--- sub02 $_[1][2]---\n";
    print "--- sub02 $_[2][0]---\n";
    print "--- sub02 $_[2][1]---\n";
    print "--- sub02 $_[2][2]---\n";
}
◆ 関数のリターン値にするケース

関数のリターン値でblessしたリファレンスが返された場合、それは、他言語でいうオブジェクトと同じ感じになるのです。

my $date = DateString->new();
print $date->to_string, "\n";

package DateString;
sub new {
    my $class = shift; # 第一引数は、クラス名が渡される。
    my $time = shift || time();
    return bless { time => $time }, $class; # blessしたリファレンスを返す。
}
sub to_string {
    my $self = shift;
    return scalar localtime $self->{time};
}



use宣言について

Perlをやり始めると、requireはすぐ分かるんだけれど、useって謎だなぁって思いますね!
今回は、そんなuseの謎に迫ります。


はじめに、requireは、C言語でいう%includeと同じで別ファイルにあるソースを実行時にあたかも自ソースとして読み込みます。
つぎに、useは、実行前コンパイル時)にあたかも自ソースとして読み込みます。


実行前というのは、Perlではコマンド起動時にコンパイルしてから実行するという2段階方式になっているのです。
つまり、useはコンパイルに実行され、requireはコンパイルに実行されるのです。


そして、読み込んだソース(モジュール)にpackage宣言がない場合、useとrequireでは、これ以外の違いはありません。
しかし、モジュールにpackage宣言があると、ちょっとややっこしくなるのです。


まず、モジュールでなく、メインソースの方には、package宣言がありませんが、実は、デフォルトでmainというpackage名になっています。
つまり、逆に言うと、package宣言のないモジュールは、mainモジュールなのです。
一方、モジュールの方は、package宣言でpackage名をmain以外で指定すると、当然ですがメインソースとは別のpackage名になりますね。


しかし、別のpackage名になっても、useには他モジュールを、あたかも自モジュール(mainモジュール)にあるようにする仕組みがあるのです。
たとえば、以下のサンプル mainモジュールとModuleモジュール をご覧下さい。

  • その仕組みは、Moduleモジュールのimportメソッドです。
  • 『use Module』は、実は『BEGIN { require Module; Module->import; }』とイコールなのです。
  • 『BEGIN』は、コンパイル時に実行されるブロックなのです。
  • 『Module->import;』は、Moduleモジュールにある sub をmainモジュールにインポートするものなのです。

いきなり、importが出てきますが、これは、Moduleモジュール内でExporterモジュールを継承していて、Exporterモジュール内のメソッドなのです。

◆サンプル

・mainモジュール

#use Module;
BEGIN { require Module; Module->import;} # use Module; と等価
print sumx(5,3),"\n"; # Moduleモジュールのsumx関数があたかも自関数のように使える


・Moduleモジュール

package Module;
use base Exporter;          # use Exporter; our @ISA = qw(Exporter); と等価
our @EXPORT = qw(&sumx);
sub sumx {
 return $_[0]+$_[1]; # 渡された引数2つを加えて返す処理
}
1;
◆補足

Moduleモジュールでは、『use base Exporter』でExporterを継承して、『our @EXPORT = qw(&sumx);』でsumx関数をエクスポートしています。
mainモジュールでは、『Module->import;』でModuleモジュールでエクスポートしたsumx関数をインポートしています。



blessについて

useやモジュールとリファレンスが理解出来てくると、blessが気になってきますね!
今回は、そんなblessの謎に迫ります。


blessを一言でいうと、リファレンスとパッケージを紐付けます。


たとえば、以下を見てください。

use strict;
use warnings;

my %test_HASH = (aaa => 'XXX', bbb =>'YYY');
my $test = bless \%test_HASH, 'package01';  # リファレンスとパッケージを紐付け

print '-->', $test->{aaa}, '<--', "\n";
$test->test01(3);
print '-->', $test->{aaa}, '<--', "\n";

package package01;
sub test01 {
    print "第1引数は$_[0], 第2引数は$_[1]です。", "\n";
    my $obj_self = shift;
    $obj_self->{aaa} = 'ZZZ';
}

『my $test = bless \%test_HASH, 'package01';』で、%test_HASHのリファレンスとpackage01のパッケージを紐付けています。
そして、blessのりターン値が%test_HASHのリファレンスなのですが、だだのリファレンスでなく、blessで紐付けられたpackage01パッケージも含まれるのです。
なので、『$test->test01(3);』のようにpackage01パッケージのtest01サブをコールすることが出来るのです。


コールする時、『->』矢印演算子を使うとtest01サブでは、自動的に第1引数が自リファレンスになるのです。
なので、『$obj_self->{aaa} = 'ZZZ';』でリファレンスからもとのハッシュ変数にアクセスして書き換えが可能なのです。


結果的にリファレンスが、ハッシュ変数(プロパティ)とサブルーチン(メソッド)を持つことになるのです。
プロパティとメソッドと言えば、そう、オブジェクトになるのですね…これが!!



オブジェクトについて

えーと、Perlでは一つのパッケージが一つのクラスになります。
つまり、パッケージ名がクラス名になるのです。


また普通、クラスには、オブジェクトを構築するコンストラクタがあります。
コンストラクタは、blessでリファレンスをクラス名に紐付けて、リターン値にします。
コンストラクタの呼び出し側では、リターン値を変数に格納して、インスタンスとします。


って、文章で説明しても分かり辛いので、以下のソースを見て感じとってください。

  • ポイント1の例は、クラス名に矢印演算子で関数をコールすると第1引数にクラス名が自動的に入ります。
  • ポイント2の例は、インスタンスに矢印演算子で関数をコールすると第1引数にオブジェクトが自動的に入ります。
  • ポイント3の例は、オブジェクトのコンストラクタ(委譲)起動やメソッド起動をしています。
use strict;
use warnings;

print "\n", '◆ポイント1の例(パッケージ名(クラス名)が第1引数)', "\n";
package00->test00(3); # クラス名に矢印演算子で関数をコールする

package package00;
sub test00 {
    print "第1引数は$_[0], 第2引数は$_[1]です。", "\n";
    my $class = shift;
    print "第1引数は $class でクラス名になります。\n"
}


print "\n", '◆ポイント2の例(blessリファリンス(オブジェクト)が第1引数---', "\n";
my %test_HASH = (aaa => 'XXX', bbb =>'YYY');
my $test = bless \%test_HASH, 'package01';

print '-->', $test->{aaa}, '<--', "\n";
$test->test01(3); # インスタンスに矢印演算子で関数をコールする
print '-->', $test->{aaa}, '<--', "\n";

package package01;
sub test01 {
    print "第1引数は$_[0], 第2引数は$_[1]です。", "\n";
    my $obj_self = shift;
    $obj_self->{aaa} = 'ZZZ';
    print "第1引数は $obj_self  でオブジェクトになります。\n"
}


print "\n", '◆ポイント3の例 オブジェクト例(委譲)---', "\n";
my $obj_package02 = package02->new(); # newはオブジェクトコンストラクタでこれを起動(委譲)する
                                      # $obj_package02にはインスタンスが格納される
print '-->', $obj_package02->{bbb}, '<--', "\n";
$obj_package02->test02(4);            # オブジェクトのメソッドを起動する
print '-->', $obj_package02->{bbb}, '<--', "\n";

package package02; # パッケージは、Perlではクラスになる
sub new {
    print "第1引数は$_[0]でクラス名です。", "\n";
    my $class = shift;
    my %test_HASH = (aaa => 'XXX', bbb =>'YYY');
    return bless \%test_HASH, $class;
}
sub test02 {
    print "第1引数は$_[0], 第2引数は$_[1]です。", "\n";
    my $obj_self = shift;
    $obj_self->{bbb} = 'ZZZ';
    print "第1引数は $obj_self  でオブジェクトになります。\n"
}



オブジェクト指向について

Perlは元々は、オブジェクト指向言語ではありません。
しかし、柔軟な言語仕様のPerlでは、オブジェクト指向言語のように振る舞うことも可能なのです。


普通、オブジェクト指向プログラミングでは、以下の要素が必要です。

  • クラス(Perlではパッケージで対応)
  • インターフェース(Perlでは不完全だけどパッケージで対応)
  • コンストラクタPerlではblessリファレンスで対応)
  • オーバーライド(Perlでは普通に対応)
  • オーバーロードPerlでは複数の同名subは無理。しかし、一つのsubでオーバーロードもどきは可能)
  • カプセル化Perlでは標準言語仕様的には難しい。しかし、Attribute::Protectedモジュール等で可能)
  • 継承(Perlではbaseプラグマで対応)
  • 委譲(Perlでは普通に対応)
  • 多態性Perlでは普通に対応)


ということで、カプセル化オーバーロード以外は、大丈夫なのです。
以下に、クラス、コンストラクタ、継承、多態性、オーバーライドを使用したサンプルを書いてみました。

◆サンプル
use strict;
use warnings;

my $obj;
my $hiki = shift;

if ($hiki eq '1') {
    $obj = Otokonoko->new();
}
else {
    $obj = Onnanoko->new();
}
print $obj->getName(), "\n";
$obj->shumi();


package Person;
sub new {
    my $class = shift;
    my $self = {
        Name => $class,
    };
    return bless $self, $class;
}
sub getName {};


package Otokonoko;
use base 'Person';
sub getName {
    my $self = shift;
    return $self->{Name};
}
sub shumi {
    print "バスケ\n";
}


package Onnanoko;
use base 'Person';
sub getName {
    my $self = shift;
    return $self->{Name};
}
sub shumi {
    print "ケーキ作り\n";
}
◆実行結果
C:\perltest>perl tatai.pl 1
Otokonoko
バスケ

C:\perltest>perl tatai.pl 2
Onnanoko
ケーキ作り
  • クラスは、Person、Otokonoko、Onnanokoのパッケージを使用する。
  • コンストラクタは、『return bless $self, $class;』でblessリファレンスをコンストラクトする。
  • 継承は、『use base 'Person';』でbaseプラグマを使用する。
  • 多態性は、動的な条件によって、予めコーディングしてあるオブジェクトのメソッドの振る舞いが替わる。
  • オーバーライドは、スーパークラスでの抽象getNameメソッドをサブクラスの具象getNameメソッドで置き換える。