3. クラスと継承

3.1. オブジェクト指向設計法

オブジェクト指向設計法とは、オブジェクト(事物)の性質を考え、抽象化(abstraction) しながら、クラスを設計することです。

  • オブジェクトは、どのような状態があるか?

  • オブジェクト間にはどのようなメッセージのやりとりがあるか?

抽象化

不要な情報を捨てて、より本質のみ焦点を当てること

ここでは、カウンターを例に考えます。カウンターをイメージしてください。

Q: カウンターの内部には、どのような情報がありますか?

これは、プロパティの設計になる 

Q: カウンターとは、どのような情報のやりとりがありますか?

これは、メソッドの設計になる

94df2aa0214642fbb8275b1cadf453f5

インターフェース

オブジェクトと外部間で情報をやりとりをするメソッドのこと(ソフトウェアの設計でもっとも重要な概念となる)

3.1.1. UMLクラス図

クラス設計は、どのオブジェクト指向プログラミングであっても、基本的に共通です。プログラミングに依存せず、クラス設計を図示する手法として、UMLクラス図があります。

a0a4db1b0e754b5cb39ffba99ec4de3f

大規模なソフトウェア開発では、まず UMLクラス図を用いてクラス設計だけ先に行います。もちろん、UMLクラス図を用いず、Python などのプログラミング言語で直接、ソースコードで設計することもあります。

UMLクラス図の設計に基づいて、Pythonクラスを定義していきましょう。

クラス設計: まず、インターフェースだけ定義します。

[1]:
class Counter(object):
    cnt : int
    def __init__(self):
        pass

    def count(self):
        pass

    def reset(self):
        pass

    def show(self):
        pass


設計と実装

クラスを設計するときは、先にインターフェースだけ定義し、メソッドの本体はpassなどにしておきます。インタフェースの設計が終わったら、passの部分を実装していきます。

オブジェクト指向プログラミングでは、クラス設計が重要になります。クラス設計がしっかりしていれば、各メソッドの実装は意外と簡単です。大規模なソフトウェア開発では、クラス設計とメソッド開発は分業して行うことも少なくありません。

Let’s try

クラス設計にしたがって、各メソッドを実装してみよう。(つまり、passの部分を意味のあるコードに書き直す。)

クラスの実装

[2]:
class Counter(object):
    cnt : int
    def __init__(self):
        self.cnt = 0

    def count(self):
        self.cnt += 1

    def reset(self):
        self.cnt = 0

    def show(self):
        print(self.cnt)


3.1.2. Counterクラスを用いたプログラミング

インスタンス化の練習を兼ねて、オブジェクト指向プログラミングを練習しておきましょう。

(例題)カウンタ

関数stat(a, c)は、カウンタcを用いて、数列a中の5の倍数の数える。 ただし、数列中に9の倍数が出現したときは、カウンタはリセットされる。 このような関数stat(a, c)を定義してみよう。

[ ]:
def stat(a: list, c: Counter):
    # 自分で考えよう

大して難しくないはずなので、いきなり、答えを書いておきます。

[4]:
def stat(a: list, c: Counter):
    for x in a:
        if x % 5 == 0:
            c.count()
        if x % 9 == 0:
            c.reset()

c = Counter()
stat(range(1, 100), c)
c.show()


0

考えてみよう

関数stat(a,c)を一切修正することなく、9の倍数でもリセットされないようにするにはどうしたらいいだろうか?(できるのだろうか?)

3.2. クラス継承

クラス継承(class inheritance) は、オブジェクト指向プログラミングの効率を向上させる手段です。 難解的には難しいところがありますが、まずはシンプルに理解しておきましょう。

(操作的意味論による解釈)クラス継承 

新しいクラスを定義するとき、既存のクラスから拡張すること 

クラス継承を用いると、Counterクラスから、2重に数えるDoubleCounterクラスをクラス継承で作ることができます。

ea79675cd436415e880e8ee2b773d8b6 

用語

オブジェクト指向言語では、継承元のクラスのことを スーパークラス(super class) 、もしくは 親クラス (parent class) 、継承されたクラスのことを サブクラス(sub class) 、もしくは 子クラス(child class) と呼びます。

3.2.1. クラス定義の例

これも具体例を考えながらみていきましょう。

ここでは、1回のcount() 操作で、2回カウントす る DoubleCounter クラスを考えます。

Counterクラスから継承

[ ]:
class DoubleCounter(Counter):
    ...

DoubleCounter は、Counterクラスのサブクラスになります。サブクラスになると:

  • スーパークラスのプロパティとメソッドを そのまま使うことができる

  • 新しくプロパティやメソッドを追加することができる

  • スーパークラスのメソッドを 異なる処理をするように書き換えることができる

  • ただし、スーパークラスのプロパティとメソッドを取り除くことはできない

DoubleCounterクラスの定義

[5]:
class DoubleCounter(Counter):
    def count(self):
        self.cnt += 2

クラス継承されるので:

count()メソッドだけ定義しなおせば良い。

他のメソッドやプロパティは Counter クラスの定義をそのまま使うことができます。

実際に試してみると:

[6]:
c = DoubleCounter()
c.count()
c.show() # 2と表示される。

2

このように、オブジェクト指向プログラミングでは、クラス継承を活用することで、既存のクラスを再利用して 効率よくクラスを定義できるようになります。

Let’s try

Counterクラスを継承して、次のようなカウンタを作ってみよう。

  • ZeroCounter: count()を呼んでも、カウントされない

  • SecureCounter: reset()を読んでも、リセットされない

3.3. ポリモーフィズム (多相性)

クラス継承には、ポリモーフィズム(多相性)という優れた効果があります。

Let’s try

次の関数stat(a, c)を一切変更することなく、9の倍数が出現しても、リセットされないようにしよう。

[7]:
def stat(a: list, c: Counter):
    for x in a:
        if x % 5 == 0:
            c.count()
        if x % 9 == 0:
            c.reset()

ヒント: reset()を無効にしたカウンタを作ります。

失敗する例: reset()のないカウンタ

[8]:
class NonResetCounter(object):
    cnt : int
    def __init__(self):
        self.cnt = 0

    def count(self):
        self.cnt += 1

    def show(self):
        print(self.cnt)


動かしてみると

[9]:
c = NonResetCounter()
stat(range(1, 100), c)
c.show()

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-9-849ff627b103> in <module>
      1 c = NonResetCounter()
----> 2 stat(range(1, 100), c)
      3 c.show()

<ipython-input-7-b123083c9840> in stat(a, c)
      4             c.count()
      5         if x % 9 == 0:
----> 6             c.reset()

AttributeError: 'NonResetCounter' object has no attribute 'reset'

次のようにエラー(AttributeError)が発生します。これは、NonResetCounterreset()がないからです。

AttributeError: ‘NonResetCounter’ object has no attribute ‘reset’

つまり、NoResetCounterクラスは、一見、Counterクラスのように見えますが、reset()がないため、Counterクラスとして役割が果たせません。

型エラー

オブジェクトの種類が異なることによって発生するエラー

一方、次のように、Counterクラスを継承し、reset()で何もしないように定義すると:

成功する例: SecureCounterクラス

[10]:
class SecureCounter(Counter):
    def reset(self):
        pass


[11]:
c = SecureCounter()
stat(range(1, 100), c)
c.show()

19

これは、SecureCounterクラスは、Counterクラスの機能を全て継承しているため、常にCounterとしての役割を果たせることが保証されます。

つまり、継承されたSecureCounterオブジェクトは、常にCounterオブジェクトの代わりに用いても 型エラーにならないことを保証されます。 このような性質を 型安全性 と呼びます。

型安全性

型エラー (type error) が発生しないことが保証される

クラス継承は、型安全で、Counter, DoubleCounter, SecureCounterのように、 様々なプログラム実行の振る舞いを変更することができます。ポリモーフィズム は、 オブジェクトが、同じインターフェースで、様々な振る舞いを実現する性質です。

3.4. コースワーク

オブジェクト指向とクラス継承は大変、強力です。 あらゆるものをモデル化することができます。

演習問題(クラス継承)

次のソースコードを読んで答えよ。

[12]:
class Expr(object):
    def eval(self):
        pass

class Val(Expr):
    value: float
    def __init__(self, value):
        self.value = float(value)
    def eval(self):
        return self.value

class BinaryExpr(Expr):
    left: Expr
    right: Expr
    def __init__(self, left, right):
        self.left = left
        self.right = right

class AddExpr(BinaryExpr):
    def eval(self):
        return self.left.eval() + self.right.eval()

class MulExpr(BinaryExpr):
    def eval(self):
        return self.left.eval() * self.right.eval()


  1. これらのクラスは、何をモデル化したものか? (ヒント: MulExpr(AddExpr(Val(1),Val(2)), Val(3)) は何?)

  2. eval(self)メソッドの役割(目的)は何か?

  3. Expr, Val, BinaryExpr, AddExpr, MulExpr の継承関係をUMLクラス図にしてみよう