M.Hiroi's Home Page

Linux Programming

お気楽 Perl プログラミング超入門

[ PrevPage | Perl | NextPage ]

オブジェクト指向 (前編)

今回から「オブジェクト指向」の話に入ります。プログラミングに興味のある方ならば、オブジェクト指向という言葉は聞いたことがあると思います。よく使われているオブジェクト指向言語にC++や Java があります。C++はオブジェクト指向プログラミングができるようにC言語を拡張したものですが、度重なる機能追加により複雑な言語仕様になってしまいました。このため、初心者がオブジェクト指向を学ぶには適していない、と言われています。

その点、Java は初めからオブジェクト指向言語として設計されたため、すっきりとした言語仕様を持っています。また、現在では Python や Ruby などオブジェクト指向をサポートしているスクリプト言語も普及しています。これらの言語は、オブジェクト指向だけではなく、プログラミング入門用の言語として推薦するユーザーも多いようです。

Perl のオブジェクト指向はバージョン 5 で追加された機能ですが、C++のような複雑なものではありません。一般のオブジェクト指向言語とはちょっと変わっていますが、Perl らしい実装方法といえるでしょう。まずはオブジェクト指向について簡単に説明します。

●オブジェクト指向とは?

拙作のページ 関数 で簡単に説明しましたが、プログラミングは模型を組み立てる作業と似ています。模型が大きくなると、一度に全体を組み立てるのは難しくなります。そのような場合、全体をいくつかに分割して、まずその部分ごとに作ります。最後に、それを結合して全体を完成させます。これは模型に限らず、あらゆる分野で使われている手法 [*1] です。

これは、プログラミングにも当てはまります。実現しようとする処理が複雑になると、一度に全部作ることは難しくなります。そこで、全体を小さな処理に分割して、個々の処理を作成します。それらを組み合わせて全体のプログラムを完成させるのです。

今までのプログラミングでは、この部品に相当するのが「関数」です。関数の役割は、入力されたデータを処理してその結果を返すことです。つまり、関数は機能を表しているので、関数を部品と見なすには少々無理があるのです。このため、全体を小さな処理に分割するにしても、機能単位で行われることが普通です。

オブジェクト指向プログラミングでは、関数ではなく「オブジェクト (object : 物体)」を部品として扱います。私達の周囲にはいろいろなオブジェクトがありますが、プログラムでいうオブジェクトとはなんでしょう。たとえば、えんぴつを考えてみます。えんぴつには、色、長さ、固さ、などいろいろな性質がありますね。そして、えんぴつを使って紙に文字を書いたり、絵を描いたりすることができます。普通のえんぴつを使えば黒い文字を、赤えんぴつを使えば赤い文字を書くことができます。プログラムでは、このような性質をデータで表し、機能を関数で表すことになります。そしてオブジェクトとは、このデータと関数を結び付けたものなのです。

今までのプログラミング言語では、データと関数を別々に定義するため、それをひとつのオブジェクトとして表すことができません。赤えんぴつで赤い文字を書くにも、えんぴつの種類をチェックして赤い文字を書くようにプログラムしなければいけません。ところが、オブジェクトはデータと関数を結び付けたものなので、自分がなにをしたらよいかわかっています。えんぴつオブジェクトに文字を書けと命じれば、それが赤えんぴつのオブジェクトであれば文字は赤に、黒えんぴつのオブジェクトであれば黒い文字となるのです。

このように、オブジェクトはデータと関数をひとつにまとめたものです。従来のプログラミングが全体を機能単位で分割するのに対し、オブジェクト指向プログラミングでは全体をオブジェクト単位に分割して、それを組み合わせることでプログラムを作っていきます。

ところで、データと関数を結び付けることは、従来のプログラミング言語でも可能です。オブジェクト指向はプログラミングの考え方のひとつであり、C++のようなオブジェクト指向言語を使わなくても、その考え方にしたがってプログラムを作れば、オブジェクト指向プログラミングができます。

実際、オブジェクト指向には様々な考え方があり、いろいろなオブジェクト指向言語が存在します。ですが、データと関数をひとつにまとめたものをオブジェクトとして扱うという基本的な考え方は、オブジェクト指向言語の元祖と言われる Smalltalk でも、C++, Java, Perl, Python, Ruby などでも同じです。

-- note --------
[*1] 分割統治法といいます。

●クラスとインスタンス

次は、一般のオブジェクト指向でよく使われる、クラス、インスタンス、メソッド、継承について簡単に説明します。

「クラス (class)」はオブジェクトの振る舞いを定義したものです。ここでデータを格納するための変数や、それを操作する関数が定義されます。クラスはオブジェクトの設計図にあたるもので、オブジェクトの「雛形」と呼ぶこともあります。クラスはオブジェクトの振る舞いを定義するだけで、アクセスできる実体はなにも生み出していない、ということに注意してください。

このクラスから実体として作り出されるのが「インスタンス (instance)」です。このインスタンスを「オブジェクト」と考えてください。インスタンスを生成する方法は、当然ですがプログラミング言語によって違います。C++や Java は new を使いますが、Common Lisp の CLOS (Common Lisp Object System) では make-instance を使います。

くどいようですが、「クラス」はオブジェクトの振る舞いを定義するだけで、実際にアクセスするオブジェクトが「インスタンス」である、ということに注意してください。次の図を見てください。


              図 : クラスとインスタンスの関係

クラスはオブジェクトの定義を表すものですから、Foo というクラスはひとつしかありません。これに対し、インスタンスはクラスから生み出されるオブジェクトです。クラス Foo に new を適用することで、いくつでもインスタンスを生み出すことができるのです。クラスは設計図であり、それに従って作られるオブジェクトがインスタンス、それを作り出す工場が new である、と考えるとわかりやすいでしょう。

●メソッド

「メソッド (method)」はオブジェクトと結びついた関数です。オブジェクト指向プログラミングでは、他の関数から直接オブジェクトを操作することはせず、メソッドを呼び出すことで行います。メソッドは、クラスが異なっていれば同じ名前のメソッドを定義することができます。たとえば、クラス Foo1 にメソッド bar が定義されていても、クラス Foo2 に同名のメソッド bar を定義することができます。

そして、ここからが重要なのですが、あるオブジェクトに対してメソッド bar を呼び出した場合、それが Foo1 から作られたオブジェクトであれば、Foo1 で定義された bar が実行され、Foo2 から作られたオブジェクトであれば、Foo2 で定義された bar が実行されるのです。このように、オブジェクトが属するクラスによって、実行されるメソッドが異なるのです。これを「ポリモーフィズム (polymorphism)」と呼びます。この機能により、オブジェクトは自分が行うべき適切な処理を実行できるわけです。

クラス、インスタンス、メソッドの関係を図に示すと、次のようになります。


         図 : クラス、インスタンス、メソッドの関係

クラスという設計図が中心にあり、そこからインスタンスが生み出され、メソッドを使ってインスタンスを操作する、という関係になります。

●継承

「継承 (inheritance : インヘリタンス)」はオブジェクト指向の目玉ともいえる機能で、簡単に言うとクラスに「親子関係」を持たせる機能です。子クラスは親クラスの性質を受け継ぐことができます。プログラミング言語の場合、引き継ぐ性質は定義されたデータやメソッドになります。プログラムを作る場合、今まで作ったプログラムと同じような機能が必要になることが多いのですが、継承を使うことでその機能を受け継ぎ、新規の機能や変更される機能だけプログラムする、いわゆる「差分プログラミング」が可能となります。

あるクラスを継承する場合、その元になるクラスを「スーパークラス」とか「ベースクラス」と呼びます。そして、継承したクラスを「サブクラス」と呼びます。この呼び方は言語によってまちまちで統一されていません。C++の場合は、元になるクラスを「基本クラス」、継承するクラスを「派生クラス」とか「導出クラス」といいます。

たとえば、クラス Foo1 を継承してクラス Foo2 を作成しました。クラス Foo1にはメソッド bar が定義されています。クラス Foo2 にメソッド bar は定義されていませんが、Foo2 のオブジェクトに対して bar を呼び出すと、スーパークラス Foo1 のメソッド bar が実行されます。

メソッドの選択は次のように行われます。まず、オブジェクトが属するクラス Foo2 にメソッド bar が定義されているか調べます。ところが、Foo2 には bar が定義されていないので、スーパークラスである Foo1 に bar が定義されているか調べます。ここでメソッド bar が見つかり、それを実行するのです。このように、メソッドが見つかるまで順番にスーパークラスを調べていきますが、最上位のスーパークラスまで調べてもメソッドが見つからない場合は、エラーとなります。

ところで、継承したクラスのメソッドとは違う働きをさせたい場合はどうするのでしょうか。これはとても簡単で、同名のメソッドを定義することで、そのクラスのメソッドを設定することができます。この機能を「オーバーライド (over ride)」といいます。メソッドを選択する仕組みから見た場合、オーバーライドは必然の動作です。メソッドはサブクラスからスーパークラスに向かって検索されるので、スーパークラスのメソッドよリサブクラスのメソッドが先に選択されるのは当然なことなのです。

●Perl のオブジェクト指向

さて、一般的な話はここまでにして、Perl のオブジェクト指向に目を向けてみましょう。 Perl の場合、クラスの定義やインスタンスを生み出すための特別の構文は用意されていません。クラスは「パッケージ (package)」で定義し、パッケージ内で定義された関数がメソッドとして扱われます。

そして、オブジェクトは、リファレンスが指し示すデータにクラスの印を付けることで表します。つまり、Perl では配列でもハッシュでもスカラー変数でも、リファレンスで指し示すことができるものであればなんでも、オブジェクトとして扱うことができるのです。

これには M.Hiroi も驚きました。C++ や Java ではクラスを定義するためのキーワード class があり、CLOS ではクラスを定義する関数 defclass があります。そして、new や make-instatnce で生成されるオブジェクトは「インスタンス」として、ほかのデータとは区別されるのが普通 [*2] です。逆に言えば、プログラミング言語によってオブジェクトの構造が決められているのですが、Perl の場合は違います。オブジェクトの構造はプログラマの都合で選択することができるのです。

まあ、なんでもありの Perl らしいオブジェクト指向といえるのですが、初心者にはプログラミングの自由度が高すぎて、どうやって使ったらよいかとまどうことにもなりかねません。M.Hiroi も 参考文献 2.『クラスはパッケージに過ぎない』 という項目を見たときは、「なんでパッケージがクラスなんだ?」と大変混乱したものです。パッケージは以前のバージョンから存在する機能ですが、ようするに、クラス定義はパッケージで代用する、という意味に過ぎなかったのです。

このように、Perl のオブジェクト指向はほかのオブジェクト指向言語とはちょっと変わっているためとまどうこともあるのですが、実際に使ってみると難しいことはありません。「簡単なことは簡単に。難しいことは可能に。」という Perl のポリシーは、オブジェクト指向にも貫かれているようです。まずは、パッケージ本来の使い方を説明しましょう。

-- note --------
[*2] これは既存の機能にオブジェクト指向を追加したハイブリッド言語の場合で、オブジェクト指向の元祖 Smalltalk や Ruby では、すべてのデータがオブジェクトとして扱われます。

●パッケージとは?

プログラムを作っていると、以前作った関数と同じ処理が必要になる場合があります。このような場合、皆さんはどうしますか。いちばんてっとり早い方法はソースファイルからその関数をコピーすることですね。ですが、ファイル数が増えてくると、そのうちに必要な関数がどのファイルに入っているかわからなくなってしまいます。もちろん grep を使って検索してもいいのですが、関数が必要になるたびに検索とコピーを繰り返すのは面倒ですね。このような場合、自分で作成した関数をライブラリとしてまとめておくと便利です。もともとパッケージは、ライブラリを作成するために使用される機能です。

ライブラリの作成で問題になるのが「名前の衝突」です。複数のライブラリを使うときに、同じ名前の関数や変数が存在すると、そのライブラリは正常に動作しないでしょう。この名前の衝突を避けるために package を使います。次の例を見てください。

リスト : パッケージの簡単な使用例

use strict;
use warnings;

package foo;
our $a = 10;

package main;
our $a = 20;

print $a;       # 20 (main パッケージの $a を表示)
print $foo::a;  # 10 (foo  パッケージの $a を表示)

package は名前が所属するグループ (名前空間 : namespace) を指定します。最初に foo というパッケージを宣言しました。なお、パッケージ名の指定には文字列を使うことはできません。ご注意ください。

package foo 以下で定義される関数や変数名はパッケージ foo に属します。次に、main というパッケージを宣言しました。Perl の場合、ひとつのファイルの中で package を複数回使ってもかまいません。そのたびに名前空間が切り替わります。したがって、2 番目の $a はパッケージ main に属する大域変数となり、最初の大域変数 $a と衝突することはありません。

print で $a の値を表示してみましょう。ここでは main パッケージのままなので、$a の値は 20 となります。ほかのパッケージの変数や関数は、「パッケージ名::名前」のようにアクセスすることができます。したがって、$foo::a は foo パッケージの変数 $a の値を表示します。foo::$a ではないので注意してくださいね。

Perl の場合、慣例としてひとつのパッケージにはひとつのファイルを割り当て、ファイル名はそのパッケージ名に合わせることになっています。したがって、package の宣言はファイルの先頭で行われることが普通です。なお、package が省略された場合、そのファイルは main パッケージとなります。

拡張子が .pm のファイルを「モジュール」と呼び、.pl のファイルを「ライブラリ」と呼びます。 Perl 5 ではモジュールを使うのが普通です。ライブラリをロードするには require を、モジュールをロードするには use を使います。ファイルがロードされる場合、そのファイルは読み込みの成否を示す値 (1 or 0) を返すことになっています。つまり、最後に実行される文は return 1; や 1; のようなものでなければなりません。ご注意くださいませ。

Perl のパッケージには、このほかにも強力な機能が備わっているのですが、とりあえず package の宣言により名前空間が切り替わることを理解すれば、オブジェクト指向機能を使うことができるようになります。

●クラスの定義とインスタンスの生成

Perl では、クラスの定義はパッケージで行います。簡単な例として、点を表す Point クラスを作ってみましょう。

リスト : Point クラス

use strict;
use warnings;

package Point;

# インスタンスの生成
sub new {
    my ($x, $y) = @_;
    my $obj = {x => $x, y => $y};
    bless $obj, 'Point';
    $obj;
}

一般に、クラス名は英大文字から始めることが多いので、パッケージ名は Point としました。関数 new はインスタンスを生成するメソッドです。new はインスタンスを操作するのではなく、クラスの動作にかかわるメソッドです。このようなメソッドを「クラスメソッド」といい、インスタンスを操作するメソッドを「インスタンスメソッド」といいます。

メソッド new は引数として座標 $x, $y を受け取り、インスタンスを生成します。前回説明したように、Perl では配列でもハッシュでもスカラー変数でも、リファレンスで指し示すことができるものであればなんでもインスタンスとして扱うことができますが、キーでデータの種類を表すことができるハッシュを使うことが多いようです。そこでインスタンスはハッシュで表し、x 座標はキー 'x' に、y 座標はキー 'y' に格納することにします。この 2 つのデータで点の座標を表します。

生成したハッシュをインスタンスとして扱うには、 関数 bless を使ってクラスの印をつけます。

bless リファレンス, クラス名

bless は、リファレンスが指し示すデータに、クラスに属するインスタンスであることを示す印を付けます。Perl では、この操作を「ブレスする」 [*3] といいます。第 1 引数にリファレンスを与え、第 2 引数にクラス名 (パッケージ名) を指定します。第 2 引数を省略するとカレントパッケージが指定されます。ただし、クラス名を省略するとメソッドの継承が動作しないことがあるので、面倒だとは思わずにクラス名を書くようにしてください。最後に、メソッド new の返り値として生成したオブジェクト返します。

メソッド new の呼び出しは、次のように行います。

my $p1 = Point::new(0, 0);

Perl の場合、メソッドは関数にすぎません。通常の関数と同じく、Point クラスで定義されているメソッド new を呼び出すことができます。ところが、クラス名を明示すると、ポリモーフィズムを利用することができません。このため、メソッドの呼び出しには特別な構文が用意されていて、そちらを使用するのが普通です。これは次のインスタンスメソッドで説明します。

-- note --------
[*3] ブレス (bless) は、清める、祝福する、という意味ですが本稿では 参考文献 2. の日本語訳に従うことにします。

●メソッドの呼び出し

それでは、インスタンスを操作するメソッドとして、2 点間の距離を計算する distance を作ってみます。当然ですが、distance はパッケージ Point 内で定義します。

リスト : Point.pm

use strict;
use warnings;

package Point;

# インスタンスの生成
sub new {
    my ($x, $y) = @_;
    my $obj = {x => $x, y => $y};
    bless $obj, 'Point';
    $obj;
}

# 2 点間の距離を計算する
sub distance {
    my ($obj1, $obj2) = @_;
    my $dx = $obj1->{'x'} - $obj2->{'x'};
    my $dy = $obj1->{'y'} - $obj2->{'y'};
    sqrt( $dx * $dx + $dy * $dy );
}

1;

メソッド distance は Point クラスのインスタンスを 2 つ受け取り、その距離を計算します。distance の定義自体は、インスタンス (ハッシュ) から座標を取り出して距離を計算するだけです。

メソッド distance は、次のように呼び出すことができます。

リスト : sample1001.pl

use strict;
use warnings;
use Point;

my $p1 = Point::new(0, 0);
my $p2 = Point::new(1, 1);

print $p1->distance($p2), "\n";
$ perl -I. sample1001.pl
1.4142135623731

Perl は配列 @INC に格納されているパスからモジュールを探します。@INC の中にカレントディレクトリはないので、このままではカレントディレクトリにあるモジュールを use で読み込むことはできません。@INC にパスを追加する方法はいくつかありますが、今回のようなサンプルプログラムであれば、Perl の起動時に -I オプションで指定すると簡単です。

perl -Idirectory

=I の後ろにモジュールがあるディレクトリを指定するだけです。たとえば -I. とすれば、カレントディレクトリにあるモジュールを use で読み込むことができます。

矢印「->」はメソッドを呼び出す構文です。最初に Perl は $p1 が属するクラスを調べます。$p1 には Point クラスのインスタンスが格納されていますね。そこで Perl は Point クラスに定義されているメソッドを調べ、該当するメソッド distance を呼び出すのです。これは、次のような関数形式の呼び出しと同じです。

Point::distacne($p1, $p2);

矢印左側のインスタンスを第 1 引数として、そのクラスのメソッドが呼び出されます。ここで、矢印を使ったメソッドの呼び出しは、クラス名を明示する通常の関数呼び出しと違い、矢印左側のインスタンスによって適切なメソッドが選択されることに注意してください。たとえば、3 次元の座標を表す Point3d クラスを考えてみましょう。

リスト : 3 次元の座標を表す (Point3d.pm)

use strict;
use warnings;

package Point3d;

sub new {
    my ($x, $y, $z) = @_;
    my $obj = {x => $x, y => $y, z => $z};
    bless $obj, 'Point3d';
    $obj;
}

# 2 点間の距離を計算する
sub distance {
    my ($obj1, $obj2) = @_;
    my $dx = $obj1->{'x'} - $obj2->{'x'};
    my $dy = $obj1->{'y'} - $obj2->{'y'};
    my $dz = $obj1->{'z'} - $obj2->{'z'};
    sqrt($dx * $dx + $dy * $dy + $dz * $dz);
}

1;

クラス Point3d は、Point を 3 次元に拡張しただけです。Point でも Point3d でも距離を計算するメソッド distance が定義されていることに注目してください。それでは、メソッド distance を呼び出してみましょう。

リスト : sample1002.pl

use strict;
use warnings;
use Point;
use Point3d;

my $p1 = Point::new(0, 0);
my $p2 = Point::new(1, 1);
my $p3 = Point3d::new(0, 0, 0);
my $p4 = Point3d::new(1, 1, 1);

print $p1->distance($p2), "\n";
print $p3->distance($p4), "\n";
$ perl -I. sample1002.pl
1.4142135623731
1.73205080756888

このように、矢印左側のインスタンスによって適切なメソッドが呼び出されています。これを「ポリモーフィズム (polymorphism)」といいます。もしも、ポリモーフィズムを利用せずにプログラムすると、distance の中でインスタンスの種類をチェックしなければいけません。インスタンスの種別を調べるには関数 ref を使います。

ref リファレンス

関数 ref は与えられた引数がリファレンスならば、リファレンスが示すデータの種別を文字列として返します。引数がリファレンスでなければ、空文字列を返します。

REF        リファレンス
SCALAR     スカラー
ARRAY      配列
HASH       ハッシュ
CODE       コード
GLOB       型グロブ

もしもリファレンスが指しているデータがブレスされていたならば、そのクラス名を返します。ref を使って distance を書き換えると、次のようになります。

リスト : ポリモーフィズムを使わない distance

sub distance {
    my ($obj1, $obj2) = @_;
    my $type = ref $obj1;
    if ($type eq 'Point') {
        my $dx = $obj1->{'x'} - $obj2->{'x'};
        my $dy = $obj1->{'y'} - $obj2->{'y'};
        sqrt($dx * $dx + $dy * $dy);
    } elsif ($type eq 'Point3d') {
        my $dx = $obj1->{'x'} - $obj2->{'x'};
        my $dy = $obj1->{'y'} - $obj2->{'y'};
        my $dz = $obj1->{'z'} - $obj2->{'z'};
        sqrt($dx * $dx + $dy * $dy + $dz * $dz);
    } else {
        die("処理できないデータです\n");
    }
}

distance は 2 つのデータを扱うだけなので、プログラムはそれほど複雑にはなりません。しかし、たくさんのデータを扱うようになると、それだけプログラムは複雑になります。とくに、新しいデータを追加するような場合、プログラムの内部でデータの種別をチェックしている箇所をすべて調べて、そこに新しい処理を追加しなければいけません。プログラムの規模が大きくなると、修正箇所を調べるだけでも大変です。

ところが、ポリモーフィズムを使ってプログラムすると、新しいデータを追加するにしても、そのデータを表すクラスとメソッドを定義するだけでいいのです。あとは Perl がインスタンスに合わせて適切なメソッドを呼び出してくれます。オブジェクト指向では、オブジェクトをひとつの部品として扱います。新しい部品を追加するにしても、今までの部品を修正せずにそのまま使えた方が便利ですね。

●クラスメソッドの呼び出し

ところで、クラスメソッド new も矢印形式で呼び出すことができます。

$p1 = Point->new(0, 0);

矢印の左側がクラス名の場合、そのクラスに定義されている関数が呼び出されます。この場合、クラス名 Point が第 1 引数として渡されることに注意してください。そこで、矢印形式で new を呼び出せるように改造してみましょう。

リスト : インスタンスの生成 (改造版)

sub new {
    my ($type, $x, $y) = @_;
    my $obj = {x => $x, y => $y};
    bless $obj, $type;
    $obj;
}

このようにクラスメソッドを定義すると、適切なクラスのインスタンスを簡単に生成することができます。たとえば、変数 $tmp にクラス名が格納されている場合、第 1 引数にパッケージ名を受け取るように new を定義すると、次のように呼び出すことができます。

$tmp->new();

これだけで $tmp に格納されたクラスのインスタンスを生成することができます。

それから第 1 引数 $type をチェックすることで、インスタンスメソッドとしての機能を持たせることもできます。たとえば、インスタンスのコピー [*4] を生成する場合にも new というメソッドを使うことにしましょう。この場合、関数 ref で第 1 引数をチェックして、返り値が空文字列であれば、引数はパッケージ名なので新しいインスタンスを生成し、そうでなければ引数はインスタンスなので、そのコピーを生成するように new を定義することができます。

このほかに、第 1 引数にパッケージ名を受け取らないクラスメソッドだと、そのクラスを継承したときに困ることがあるのです。詳しい内容は継承のときに説明しますが、メソッドは第 1 引数にクラス名かオブジェクトを受け取るように作ることにします。

-- note --------
[*4] オブジェクト指向では「クローン(clone)」と呼ぶことがあります。

初版 2015 年 5 月 3 日
改訂 2023 年 3 月 19 日

Copyright (C) 2015-2023 Makoto Hiroi
All rights reserved.

[ PrevPage | Perl | NextPage ]