Pythonプログラミング(ステップ8・クラスとオブジェクト・その1)

このページでは、クラス定義とオブジェクトの使い方の基礎を学ぼう。

1.オブジェクト = 動詞 + 目的語 のパッケージ

コンピュータが行う処理は、基本的に、『OにたいしてVする』という形式に還元できる。 Oはデータで、Vはそれに対して何をするか(関数=「動詞」)、である。 ここで、ちょっと考えてみると、OとVは、いつも対になって扱うべきであることが判る。 例えば、『ある整数にたいして絶対値を計算する』と、『ある複素数にたいして絶対値を計算する』では、具体的な計算の手続きが異なる。 『人名のリストを小さい順に並べる』、と、『数値のリストを小さい順に並べる』では、 「小さい」の解釈がデータの種類によって変わる(名前の「小さい」は、通常、アルファベット順、あるいは「あいうえお」順、と解される)。

話を簡単にするため、右はOOPの一端しか述べていないが、当面はこれで十分。

そこで、O(目的語)とV(動詞)を一体のものとして取り扱う流儀が、計算機科学の世界では普及しており、 そうした流儀で行うプログラム開発は「オブジェクト指向プログラミング(object oriented programming; OOP)」 と呼ばれている。 OOPは難解なものではなく、対象を抽象化・モデル化する際の、ごく自然な発想といえる。

ここで、目的語(O)の内容について考えてみよう。 例えば、「複素数」は実部と虚部という二つの実数の組であるし、 「人名リスト」の人名は、姓と名にさらに分けられる。 このように、目的語は一般に構造を持ったデータとなる。

そうすると、処理のパッケージ(「オブジェクト」と呼ばれる)は

変数やその組み合わせ
それら変数に対する操作を含むような関数の集まり

で構成されることになる。

たとえば、「クラスの名簿」というパッケージ(オブジェクト)があったとしよう。 このオブジェクトは

姓と名からなる名前のリスト
『リストに氏名を追加する』関数
『リストから氏名を削除する』関数
『リストを一覧にしてプリントする』関数
『リストの氏名をあいうえお順に並べ替える』関数
・・・

から成る、といった具合である。

2.クラスの定義とオブジェクトの生成

「クラスの名簿」を考えたとき、1年A組の名簿、1年B組の名簿、・・といったように、 「仕様」はまったく同じでも実体の異なるオブジェクトが複数存在してよい。 そこで、オブジェクトを使って処理を行う際には

  1. オブジェクトの仕様を記述する:クラス(class)の定義
  2. 必要に応じてオブジェクトを生成する:インスタンス化

という段取りを踏む。例えば、「2次方程式クラス」を定義して、 インスタンス化し、使う例を以下に示す:

例題13 (ex13.py)

# coding: utf-8
import math

class QuadraticEquation:
    def __init__(self,a,b,c):
        self.a=a
        self.b=b
        self.c=c

    def roots(self):
        d = self.b**2 - 4*self.a*self.c
        if d<0:
            return None
        else:
            r1 = (-self.b + math.sqrt(d))/(2*self.a)
            r2 = (-self.b - math.sqrt(d))/(2*self.a)
            return r1,r2

# メイン部

qe = QuadraticEquation(1,-5,6)

res = qe.roots()

if res is not None:
    print(res[0],res[1])
クラスの定義

クラスの名前はMyClassNameのように、大文字区切り(CapWords形式)で書くのが一般的な作法

クラス(オブジェクトの「仕様書」)の定義は

class クラス名:
   変数や関数(メソッド)の定義

の形式で行う。上の例では QuadraticEquationというクラスを定義している。

コンストラクタとメソッドの定義

QuadraticEquationクラスの中で、二つの関数(クラス内で定義する関数をメソッドと呼ぶ)を定義している。 その最初は、コンストラクタと呼ばれ、少々見慣れない形をしている:

    def __init__(self,a,b,c):
        self.a=a
        self.b=b
        self.c=c

この関数は、このクラスからオブジェクトが生成(construct)される際に、一度だけ呼ばれる。 上の例では、メイン部の qe = QuadraticEquation(1,-5,6) が、そのタイミングとなる。 コンストラクタの名前は、必ず "__init__( )" である。

コード内に "self" という変数が各所で使われているが、それは「このオブジェクト」を意味する。 同じクラス(仕様)の複数のオブジェクトが生成できるため、それぞれを異なる実体として区別するため、 Pythonではこのself(「自分」)という記号を用いる作法が採られている。 メソッドの定義では、1番目の引数を、必ずselfにしなければならない。

また、オブジェクト内の変数も、self.変数名 という要領で、self.付きで指定する。

クラスのインスタンス化

上の例では、メイン部の最初で、クラス名(コンストラクタへの引数)

qe = QuadraticEquation(1,-5,6)

によって、QuadraticEquationクラスのオブジェクトを生成し、変数 qe に(オブジェクトの所在地を)代入している。 1, -5, 6が、コンストラクタの(selfを除いた)3つの引数に引き渡されている。 qeは、$x^2 - 5 x + 6 = 0$という方程式を、その解法といっしょにパッケージしたようなモノである。

実際に、求根するには、オブジェクト.メソッド名([引数])の形式で

res = qe.roots()

とすれば、結果がリストとして得られる。上の例では、判別式が負の場合はNoneを返す ようにしているので、実根が得られた場合のみ、結果をプリントするようにしている。

ここでNoneは、Pythonに元々組み込まれている「空」を表す特殊なオブジェクト。

icon-pc 練習:三角形オブジェクト

QuadraticEquationクラスの例を参考に、以下の仕様のTriangleクラスを作成してみなさい。

Triangleクラス:
  三角形の3辺の長さをデータとして持つ
  area()メソッドによって、その面積が得られる
  type()メソッドによって、「鋭角」「鈍角」「直角」の別がそれぞれ -1, 0, 1 の数値として得られる

ひな形

# coding: utf-8
import math

class Triangle:
   ...
   ...
   ...
   ...
   ...
   
# メイン部
tr = Triangel(....)

print("面積=",tr.area())
print("種類=",tr.type())

3.オブジェクト間の系統樹:継承

生物の系統樹を考えてみよう。我々ホモ・サピエンスは、真核生物であって、動物界、脊索動物門、哺乳網、霊長目、ヒト科、ヒト属に属する生物である。 誰でも知っているように、生き物としての「作り」は、ヒト以外の生物と共通の部分が多く(ほとんど同じで)、 そうした類似性は、進化の過程での枝分かれと深く関係している。

OOPでは、系統樹に似た発想で、オブジェクトの抽象化が行われている。

Pythonにおいて、もともと、数値はオブジェクトとして扱われているが、 ここではそれを明示的にコード化してみる。

以下のコードは、複素数、実数、有理数、整数、そして自然数を、Pythonのオブジェクトとして表現した例である。 集合として、複素数は実数を含む。 そこで、実数を表すクラス RealNumber のクラス宣言では class RealNumber(ComplexNumber):のように 書いて、それがComplexNumberクラスを継承(inherit)していることを明示している。 以下、実数と有理数等の関係についても同様である。

例題14 (ex14.py)

有理数については、分子と分母の最大公約数をユークリッドの互除法で求め、 それによって約分した数値をデータとして保持するように設計。

# coding: utf-8

import math

class ComplexNumber:
    def __init__(self,x,y):
        self.real=x
        self.imag=y

    def abs(self):
        return math.sqrt(self.real**2 + self.imag**2)

class RealNumber(ComplexNumber):
    def __init__(self,x):
        super().__init__(x,0)

class RationalNumber(RealNumber):
    def __init__(self,n,m):
        d = self.gcd(abs(n),abs(m))
        self.numerator=n//d
        self.denominator=m//d
        super().__init__(n/m)

    def gcd(self,n,m):
        if n<m:
            n,m=m,n
        if (m==0):
            return n
        else:
            r = n%m
            return self.gcd(m,r)

class Integer(RationalNumber):
    def __init__(self,i):
        super().__init__(i,1)

class NaturalNumber(Integer):
    def __init__(self,n):
        if n>0:
            super().__init__(n)

# メイン部

n = Integer(-3)
print(n.abs())

上のコードのメイン部を見ると、-3というIntegerオブジェクトを生成(インスタンス化)し、変数nにセットしてから、 メソッド abs() を呼び出している。 ところが、Integerクラスの中では、abs()は定義されておらず、 実際にコードが記述されているのは ComplexNumberクラスの中である。 ここで、IntegerクラスはRationalNumberクラスを継承し、RationalNumberクラスはRealNumberクラスを継承、そして、 RealNumberクラスはComplexNumberクラスを継承しているので、 ComplexNumberクラスのabs()が実行されるということになる。

このように、あるクラスを継承したクラスのオブジェクトは、「上位」のクラスで定義されたデータや関数(メソッド)を使用することができる。 より汎用性・一般性の高い内容を上記のクラスで定義し、個別的な内容はそれを継承するクラスで実装すると、 少ないコードですっきりと事象が表現できる場合が多い。

上記のコードで、例えば、RealNumberクラスのコンストラクターは

def __init__(self,x):
    super().__init__(x,0)

と書かれているが、これは、上位のクラス(super( )によって表現)のコンストラクター(__init__(x,y))の呼び出しを意味している。 『実数とは、虚部が0の複素数である』と解釈できるので、実数値を実部、虚部を0として、オブジェクトの生成を行っているわけである。