Web Analytics Made Easy - StatCounter

工業大学生ももやまのうさぎ塾

うさぎでもわかるをモットーに大学レベルの数学・情報科目をわかりやすく解説! 数式が読み込まれない場合は1回再読み込みしてみてください。

うさぎでもわかるソフトウェア工学 Part06 オブジェクト指向とPython

こんにちは、ももやまです。

今回は、基本情報でも出てくる「オブジェクト指向」の特徴、具体的には、

  • 今までの考え方とオブジェクト指向の違い
  • クラスとインスタンスの違い
  • 汎化と特化
  • 継承
  • 集約と分散
  • オーバーライド
  • ポリモフィズム

についてPythonを使いながらわかりやすく説明していきたいと思います。

Pythonを使ったことがない人でもわかりやすく説明しているので、ぜひご覧ください!

1. 従来の考え方とオブジェクト指向の違い

まずは、オブジェクト指向という考え方が従来(手続き型)の考え方とどう違うのかを簡単にですが説明していきたいと思います。

(1) 従来(手続き型)の考え方

手続き型とは、処理の流れ、順番に注目してプログラムを書いていく方法です。

例えば、「リストの平均を表示する」手続き(処理手順)は、

  1. リスト内にある数値の合計を求める
  2. リストの要素数を求める
  3. (合計)/(要素数)を計算
  4. 3の計算結果を出力

の4ステップで書くことができます。

これをPythonで書いてみると…

data = [78,62,73,62,74,65,68] # この配列の平均を求める

# 1. 合計を求める
total = 0.0
for x in data:
    total += x

count = len(data) # 2. 要素数を求める

ave = total / count # 3. 平均値を計算
print(ave) # 4. 平均値を計算

このように書くことができます!

上のような単純なプログラムであれば、プログラムを見ただけで「あ、こんな処理するんだ!」ってわかりますよね。

このように、人間がプログラムを書いたり読んだりするのが楽なのが手続き型のメリットです。

(2) なぜオブジェクト指向が必要なのか

手続き型は人間にとって考えるのが楽だからオブジェクト指向とかいうよくわからないものなんて覚えなくてもいいじゃんと思うかもしれません。

しかし、もっと大きなプログラムになったらどうでしょうか。例えば電卓プログラムを想像してみましょう。

電卓プログラムを作るためには、少なくとも

  • 数字ボタンで数字を入力
  • 四則演算ボタンで計算内容を指定
  • イコールボタンで実際に計算をする
  • 小数点ボタンが押されたら小数点入力する

の機能をプログラミングする必要があります。

これらを手続き型、つまり処理の流れに注目して書くのはちょっとめんどくさい気分になりますよね。

たとえ手続き型で書けたとしても「何かしらのバグが発生したときのデバッグ」や「新しい機能を追加」しようとしたら頭がおかしくなりそうです。


このように、手続き型によるプログラミングには人間が記述するのは楽な一方、

  • 複数の要素が絡み合った複雑なプログラムには向いていない
  • デバッグやメンテナンスがしにくい

などの欠点があります。


そこで、

  • 複数の要素が絡み合った大規模で複雑なプログラム向け
  • デバッグやメンテナンスがより簡単

オブジェクト指向と呼ばれるプログラミング方法が考えられました*1

(3) オブジェクト指向とはなにか

オブジェクト指向のオブジェクトとは、現実世界にある「もの」のことです。

具体的には複数の異なる変数を組み合わせてできた新しい箱に相当するデータと、データを操作するいくつかのメソッド(手続き)を1つにまとめて抽象化したものです。

f:id:momoyama1192:20200712024326g:plain

C言語で構造体を習った人であれば、構造体に足(操作する手続き)が生えたものという説明のほうがわかりやすいかもしれません。

f:id:momoyama1192:20200712024331g:plain

つまりオブジェクト指向とは、現実世界にある「もの」と、ものを操作する方法の2つを意識してプログラミングをしていく方法、より詳しく説明すると「ある箱(データ)」と「箱を操作する関数(メソッド)」の2つに注目してプログラミングをしていく考え方なのです。


次の章からは、オブジェクト指向の特徴(メリット)を実際にPythonを使いながら説明していきます。

2. クラスとインスタンス

(1) クラスとは

クラスとは、オブジェクトが持つ性質、つまり

  • 箱(データ)
  • 箱を操作する手続き(メソッド)

をまとめて定義したものです。

クラスは設計図・ひな型だと思っておきましょう。

(2) インスタンスとは

インスタンスとは、クラス(設計図・ひな形)に対して具体的な値を与えることで実体化されたものを表します。

f:id:momoyama1192:20200712024321g:plain

(3) Pythonで確認

では、実際にPythonでクラスとインスタンスを作る体験をしましょう。

Step1. クラスを定義しよう

まず、冒険者クラス Player を定義してオブジェクトの(データとメソッド)ひな形を作ってみましょう。

class Player(): # クラス「冒険者(プレイヤー)」
    # インスタンスを生成したときに勝手に呼び出される
    def __init__(self,name,level,hp,mp,attack,defence):
        self.name      = name    # 名前
        self.level     = level   # レベル
        self.maxhp     = hp      # 最大HP
        self.hp        = hp      # 現在HP
        self.maxmp     = mp      # 最大MP
        self.mp        = mp      # 現在MP
        self.attack    = attack  # 攻撃力
        self.defence   = defence # 防御力
    def introduction(self,opponent):
        print("私の名前は" + self.name + "。よろしくね" + opponent + "さん。")
    # ステータス情報
    def disp_info(self):
        print("-----ステータス情報-----")
        print("名前: %s" % self.name)
        print("Level: %2d" % self.level)
        print("HP: %3d/%3d" % (self.hp,self.maxhp))
        print("MP: %3d/%3d" % (self.mp,self.maxmp))
        print("攻撃力:%3d" % self.attack)
        print("防御力:%3d" % self.defence)
    # ダメージを与える (xだけ HP を減らす)
    def damage(self,x):
        if x > self.hp:
            self.hp = 0
            print("%sに%dのダメージ!" % (self.name,x))
            print("%sはちからつきた…" % self.name)
        else:
            self.hp -= x
            print("%sに%dのダメージ!" % (self.name,x))

クラス(ひな型)を作る際に、「オブジェクト(もの)が持つ特徴」を考え、特徴を説明するのに必要なデータ、メソッドを定義していくのがコツです。例えば「冒険者」の特徴であれば、

  • 名前が付けられている
  • いろんな人と話す
  • モンスターと戦う
  • 各地を旅する

のように列挙していきましょう。

初期化メソッド init(コンストラクタ)

Pythonでは、インスタンス(実体)を生成するとき __init__ 関数が勝手に呼び出されるので、箱に入れる変数や初期値は __init__ の中に入れちゃいましょう。


なお、この初期化メソッドのことをコンストラクタと呼ぶので頭にいれておきましょう。

なお、他のオブジェクト指向言語にもコンストラクタはあります。

メソッドの引数について

メソッドの引数は、最初の1つは自身のオブジェクトを示す self で固定されています(つまり必須)。

もし引数が必要であれば、2番目以降に記していきましょう。


例えば、自己紹介をする introduction 関数では相手の名前を入れる変数 opponent を引数とするので、

    def introduction(self,opponent):

と1つ目の引数にオブジェクト自身を示す必須変数 self、2つ目の引数に相手の名前の引数 opponent を指定しています。

Step2. インスタンスを1つ作ってみよう

クラス(ひな形)が出来たので、実体化させてインスタンス party1 を1つ作ってみましょう。

先程作ったクラスの下に、下のようなコードを追加してください。

party1 = Player("ももやま",10,50,20,40,30) # クラスからひな形(インスタンス)を作る処理
# 名前: ももやま, HP: 50   MP: 20, 攻撃力: 40  防御力: 30 の冒険者 party1 が実体化された!

この1行で冒険者 party1 が完成です!

(このインスタンスを作る際にコンストラクタ、つまり初期化関数が動いています)

Step3. メソッドでインスタンスを操作しよう

このまま実体化させたインスタンスを作るのももったいないので、実際にメソッドを使ってインスタンスを操作してみましょう。

Step2で作ったコードに、操作内容を追加していきます。

party1.introduction("るか") # るかさんに自己紹介
party1.disp_info() # ステータス表示

party1.damage(10) # 毒沼を踏んで10ダメージ!
party1.disp_info() # HPが減ってるか確認

追加したコードを実行すると……

私の名前はももやま。よろしくねるかさん。
-----ステータス情報-----
名前: ももやま
Level: 10
HP:  50/ 50
MP:  20/ 20
攻撃力: 40
防御力: 30
ももやまに10のダメージ
-----ステータス情報-----
名前: ももやま
Level: 10
HP:  40/ 50
MP:  20/ 20
攻撃力: 40
防御力: 30

うまくメソッドが動いていることがわかりますね!

3. カプセル化

(1) カプセル化とは

オブジェクト指向では、データとメソッドをオブジェクトという1つのカプセル(のようなもの)にまとめて管理されています。

オブジェクトの中身をカプセルで隠すことにより、カプセルの外(つまり外部)から勝手にデータの中身を見たり、データの中身を書き換えたりすることができなくなります*2

このように、外部からオブジェクトの中身を隔離する(情報隠蔽する)機能のことをカプセル化と呼びます。

f:id:momoyama1192:20200712024401g:plain


とはいってもわかりにくいかもしれないので、具体例を1つ出しましょう。

例えば「LINE」ってアプリがありますよね。

皆さんは、LINEを使って色んな友達、家族、恋人など様々な人とメッセージを送り合ったり通話していると思います。

でも別にLINEがどんなアルゴリズムで動いてるかとか、どうやって通信されているかなんて全く知りませんよね。


この知るべき情報(操作方法)さえ知っていれば、知るべき情報以外*3は知らなくても操作できるよ、っていうのがまさにカプセル化なのです!

f:id:momoyama1192:20200712024407g:plain

(2) カプセル化のメリット

カプセル化のメリットは2つあります。

その1 情報隠蔽による不正アクセスの防止

オブジェクトがカプセルというバリアに包まれているので、カプセルの外から勝手にデータを見たり、書き換えたりすることができなくなります

そのため、外部からオブジェクトの中身を隔離し、情報隠蔽を行うことで外部からの不正アクセスを防止することができます。

これが1つ目のメリットです。

その2 メンテナンスしやすくなる

カプセル化されると、オブジェクト内でどんな風にデータが管理されているのかは我々ユーザーにはわかりません。

なので、オブジェクト内でデータの管理方法を少し変えたり、メソッドの処理内容を変えたとしても使い方さえ変わらなければ*4我々ユーザーには影響がありません。

当然「新しい機能を追加しよっかな~」ってメソッド(手続き)を付け加えても問題ありません。

このようなメンテナンスをしやすくなる(つモジュールの独立性が高くなる)点が2つ目のメリットとなります。

(3) Pythonで確認

早速Pythonでカプセル化を試してみましょう。

カプセル化されていない状態

初めにカプセル化をしていない状態を見ていきましょう。

プログラムの内容としては、まずクラス Player からインスタンス party1 を生成します。さらに、 party1 の値を直接参照したり、勝手に書き換えています。

class Player(): # クラス「冒険者(プレイヤー)」
    def __init__(self,name,level,hp,mp,attack,defence):
        self.name      = name    # 名前
        self.level     = level   # レベル
        self.maxhp     = hp      # 最大HP
        self.hp        = hp      # 現在HP
        self.maxmp     = mp      # 最大MP
        self.mp        = mp      # 現在MP
        self.attack    = attack  # 攻撃力
        self.defence   = defence # 防御力
    # ステータス情報
    def disp_info(self):
        print("-----ステータス情報-----")
        print("名前: %s" % self.name)
        print("Level: %2d" % self.level)
        print("HP: %3d/%3d" % (self.hp,self.maxhp))
        print("MP: %3d/%3d" % (self.mp,self.maxmp))
        print("攻撃力:%3d" % self.attack)
        print("防御力:%3d" % self.defence)

        
# カプセル化されていない状態
party1 = Player("ももやま",10,50,20,40,30) # インスタンス party1 を生成

print(party1.hp) # party1 の残りHPを直接アクセスすることでチェック
party1.level = 99 # party1のレベルを99に
party1.disp_info() # 値が書き換わっているか確認

このプログラムを実行すると、

50
-----ステータス情報-----
名前: ももやま
Level: 99
HP:  50/ 50
MP:  20/ 20
攻撃力: 40
防御力: 30

となり、直接データを参照したり書き換えたりできてしまっている状態、つまり情報隠蔽が出来ていない状態なことがわかります。

これではカプセル化が出来てるとは言えません。不正し放題です。

カプセル化をしてみよう

カプセル化をするためには、外部からデータを勝手に閲覧したり書き換えたりできないようにすればいいのでしたね。

Pythonでは、変数名の前に __ (アンダーバー2つ)を加えることで外部からデータを直接参照したり書き換えたりできなくなります*5


ということで、クラス内の変数(データ)をすべて外部から直接見れないようにしてあげましょう。

class Player(): # クラス「冒険者(プレイヤー)」
    # 外部からデータを見れないように(private宣言)
    def __init__(self,name,level,hp,mp,attack,defence):
        self.__name      = name    # 名前
        self.__level     = level   # レベル
        self.__maxhp     = hp      # 最大HP
        self.__hp        = hp      # 現在HP
        self.__maxmp     = mp      # 最大MP
        self.__mp        = mp      # 現在MP
        self.__attack    = attack  # 攻撃力
        self.__defence   = defence # 防御力
    # ステータス情報
    def disp_info(self):
        print("-----ステータス情報-----")
        print("名前: %s" % self.__name)
        print("Level: %2d" % self.__level)
        print("HP: %3d/%3d" % (self.__hp,self.__maxhp))
        print("MP: %3d/%3d" % (self.__mp,self.__maxmp))
        print("攻撃力:%3d" % self.__attack)
        print("防御力:%3d" % self.__defence)
    # メソッドも __(アンダーバー2つ)付けると外部からは見えなくなる
    def __debug(self,hp):
        self.__hp = hp 

        
party1 = Player("ももやま",10,50,20,40,30) 
print(party1.__hp) # party1 の残りHPを直接アクセスすることでチェック
party1.__level = 99 # party1のレベルを99に

このプログラムを実行しようとすると…

Traceback (most recent call last):
  File "object2.py", line 27, in <module>
    print(party1.__hp) # party1 の残りHPを直接アクセスすることでチェック
AttributeError: 'Player' object has no attribute '__hp'

__hp なんて存在しませんよ~」ってエラーを返します。

つまり、外部からデータを隠している状態、つまり情報隠蔽が出来ていることがわかりますね!


なお、変数名だけでなく、上の __debug のようなメソッド名(関数名)の前に __ (アンダーバー2つ)を加えることで、メソッドも隠すことができます*6


もちろん、外部からデータを隠している状態であっても、

class Player(): # クラス「冒険者(プレイヤー)」
    def __init__(self,name,level,hp,mp,attack,defence):
        self.__name      = name    # 名前
        self.__level     = level   # レベル
        self.__maxhp     = hp      # 最大HP
        self.__hp        = hp      # 現在HP
        self.__maxmp     = mp      # 最大MP
        self.__mp        = mp      # 現在MP
        self.__attack    = attack  # 攻撃力
        self.__defence   = defence # 防御力
    # ステータス情報
    def disp_info(self):
        print("-----ステータス情報-----")
        print("名前: %s" % self.__name)
        print("Level: %2d" % self.__level)
        print("HP: %3d/%3d" % (self.__hp,self.__maxhp))
        print("MP: %3d/%3d" % (self.__mp,self.__maxmp))
        print("攻撃力:%3d" % self.__attack)
        print("防御力:%3d" % self.__defence)
    # メソッドも __(アンダーバー2つ)付けると外部からは見えなくなる
    def __debug(self,hp):
        self.__hp = hp 


        
# カプセル化された状態
party1 = Player("ももやま",10,50,20,40,30) 
party1.disp_info() # メソッドからであれば __ (アンダーバー2つ)付いた変数にアクセスできる

このように公開されているメソッドからであれば問題なく __ が最初についた変数にもアクセスすることができます。

3. 汎化と特化(クラスの継承)

(1) スーパークラスとサブクラス

クラスは、オブジェクト(現実世界にあるもの)を抽象化して作られたひな形でしたね。

しかし、ひな形とは言ってもいろんな種類がありますよね。

例えば、「冒険者」であれば、「勇者」、「シーフ」、「モンク」、「白魔道士」、「黒魔道士」など様々な「冒険者」がいます。

「車」であれば、「乗用車」、「タクシー」、「バス」、「トラック」ような様々な「車」の種類があります。


このように、クラスの具体例をより下位のクラスと考えることで上位クラス - 下位クラスという関係を持たせることができます。

また、このときの上位クラスのことをスーパークラス、もしくは基底クラスと呼び、下位クラスのことをサブクラス、もしくは派生クラスと呼びます。


サブクラスとスーパークラスでは、「サブクラス is a スーパークラス」のような is-a 関係が成立するのが特徴です。

例えば、「白魔道士」と「冒険者」であれば「白魔道士 is a 冒険者(白魔道士は冒険者である)」と正しい文になるのでis-a関係、つまりサブクラスとスーパークラスの関係が成り立っていることがわかります。

f:id:momoyama1192:20200712024341g:plain

同じように「バス」と「車」であれば「バス is a 車(バスは車である)」となるのでis-a関係が成立しているといえます。

f:id:momoyama1192:20200712024346g:plain

(2) 汎化と特化の関係

汎化

サブクラス(下位のクラス)が持つ性質を抽出し、抽出したものをスーパークラス(上位のクラス)として定義することを汎化と呼びます。

特化

スーパークラス(上位のクラス)の具体例をサブクラス(下位のクラス)として定義することを特化と呼びます。

f:id:momoyama1192:20200712024351g:plain

汎化と特化はごちゃまぜになりやすいので頭に入れる際には注意しましょう。

(3) 継承(インヘリタンス)

サブクラスは、上位のスーパークラスの具体例の1つとして定義されているので、スーパークラスと同じような特性を持つことも多いです。

例えば、例えば「車」には、

  • アクセルを踏んだら加速する
  • ブレーキを踏んだら減速する
  • 走るとガソリンを使う

などの特性がありますが、これはサブクラスの「バス」、「タクシー」、「トラック」でも全部成立する特性ですよね。


しかし、サブクラスを定義する際にいちいちこの特性を再定義するのはめんどくさいですよね。

そこで、サブクラス(下位のクラス)はスーパークラス(上位のクラス)の特性(データ・メソッド)を引き継いで使う機能がオブジェクト指向では導入されました。この機能のことを継承(インヘリタンス)と呼びます。

継承機能を使うと、サブクラスは何もプログラミングしなくても継承元(スーパークラス)のデータやメソッドをすべて利用することができます。


そのため、サブクラスはスーパークラスにない不足するデータ、メソッドを追加するだけでクラス、つまりひな形を作ることができます*7

(4) Pythonで継承を試してみよう

早速Pythonで継承機能を試してみましょう。

今回は、最初に定義した冒険者クラス Player を黒魔道士クラス BlackMage に継承しています。

(つまり、Player クラスの機能(データ、メソッド)が BlackMage クラスでそのまま使えるようになっています)

# 冒険者クラス(スーパークラス)
class Player(): 
    def __init__(self,name,level,hp,mp,attack,defence):
        self.name      = name    # 名前
        self.level     = level   # レベル
        self.maxhp     = hp      # 最大HP
        self.hp        = hp      # 現在HP
        self.maxmp     = mp      # 最大MP
        self.mp        = mp      # 現在MP
        self.attack    = attack  # 攻撃力
        self.defence   = defence # 防御力
    # ステータス情報
    def disp_info(self):
        print("-----ステータス情報-----")
        print("名前: %s" % self.name)
        print("Level: %2d" % self.level)
        print("HP: %3d/%3d" % (self.hp,self.maxhp))
        print("MP: %3d/%3d" % (self.mp,self.maxmp))
        print("攻撃力:%3d" % self.attack)
        print("防御力:%3d" % self.defence)

        
# 黒魔道士クラス(サブクラス)
# スーパークラスである冒険者クラスを継承
class BlackMage(Player): # 継承するクラスは括弧内 () に書く 
    # costだけMPを消費して魔法を唱えて(爆発させる)メソッド
    def use_magic(self,cost):
        if cost > self.mp:
            print("MPが足りない!")
        else:
            self.mp -= cost # MP消費
            print("MPを%d消費して魔法を唱えた!(残りMP:%d)" % (cost,self.mp))
            print("あたり一面で爆発が起こった!")
    # ※コンストラクタ(__init__関数)も継承されるので書かなくてOK(そのまま使う場合は)

party2 = BlackMage("るか",12,45,30,30,25) # Player(冒険者)クラスを継承しているので、__init__ もPlayerクラスのものを使える(もう1回書かなくてもOK)
party2.disp_info()  # Player(冒険者)クラスを継承しているので、Playerクラスのメソッドを使える
party2.use_magic(3) # Mageクラスで新たに定義したメソッドを利用

このプログラムを実行すると…

-----ステータス情報-----
名前: るか
Level: 12
HP:  45/ 45
MP:  30/ 30
攻撃力: 30
防御力: 25
MPを3消費して魔法を唱えた!(残りMP:27)
あたり一面で爆発が起こった!

このように、うまく継承ができていることがわかりますね!

4. 集約と分解

複雑なオブジェクトをそのまま考えると少し頭が混乱することがあります。

そこで、複雑なオブジェクト(上位のクラス)を単純ないくつかのオブジェクト(下位のクラス)に分解する方法が考えられました。

(1) 集約とは

単純なクラス(下位クラス)をいくつか集めてより複雑なクラス(上位クラス)として定義することを集約と呼びます。

(2) 分解とは

複雑なクラス(上位クラス)をいくつかの単純なクラス(下位クラス)にバラバラにして定義することを分解と呼びます。

f:id:momoyama1192:20200712024356g:plain

(3) 集約と分解で成り立つ関係 part-of

集約と分解、には part-of の関係が成り立ちます。

例えば、「ハンドル」と「車」であれば「ハンドル (is) part of 車(ハンドルは車の一部である)」のように正しい文になるため、part-of関係が成立していることがわかりますね。

また、「ライス」と「ランチ」であれば「ライス (is) part of ランチ(ライスはランチの一部である)」となり、part-of関係が成り立ちます。


なお、集約と分解では is-a 関係は成り立たないので継承関係もありません。

(例えば「ハンドル is a 車(ハンドルは車である)」は文章的に意味不明になるので is-a 関係は成り立たちませんよね。)

5. オーバーライド

(1) オーバーライドとは

オブジェクト指向では、スーパークラス(上位のクラス)の特性(データ・メソッド)をサブクラスに引き継げる継承という機能があります。

しかし、継承したメソッドの中に、サブクラスで改めて定義したいメソッドがあるとします。


そのときに、改めて定義したいメソッドを名前を変えずにサブクラスで定義することオーバーライドと呼びます。

(2) オーバーロードとの違い

オーバーライドに似たような言葉にオーバーロードがあります。

オーバーロードとは、引数、返り値が違う同じ名前のメソッドを複数定義するオブジェクト指向で使われる方法の1つです*8

オーバーライドとは全然関係のない言葉なので、覚え間違えないようにしましょう。

なお、覚える際には、「オーバーライドのライドは上書きって意味だからスーパークラスのメソッドを上書きしてサブクラスで定義することだな…」、「オーバーロードのロードは読み込みって意味だから引数、返り値が違う同じ名前のメソッドを複数読み込んで定義することだな…」と覚えると間違えないと思います。

(3) Pythonで確認

では、Pythonでオーバーライドについて確認していきましょう。

# クラス「冒険者(プレイヤー)」
class Player():
    def __init__(self,name):
        self.name      = name    # 名前
    # 自己紹介関数、引数 opponent には相手の名前を入れる
    def introduction(self,opponent):
        print("私の名前は" + self.name + "。よろしくね" + opponent + "さん。")

class Monk(Player):
    def __init__(self,name,power): # 初期化関数をオーバーライド
        self.name      = name    # 名前
        self.power     = power   # 拳の強さ

# クラス「魔道士」(Playerを継承)
class Mage(Player): 
    def introduction(self,opponent): # 自己紹介関数をオーバーライド
        print("私の名前は" + self.name + "です。" + opponent + "さん、これからよろしくお願いします!")

# クラス「忍者」(Playerを継承)
class Ninja(Player):
    def introduction(self,opponent): # 自己紹介関数をオーバーライド
        print("拙者、" + self.name + "と申す。" + opponent + "殿、よろしくでござる。")



party1 = Player("アルク")   # 冒険者クラス
party2 = Monk("ケン",5000) # モンククラス(初期化関数をオーバーライド)
party3 = Mage("レフィア")   # 魔道士クラス
party4 = Ninja("イングス")  # 忍者クラス


party1.introduction("ルーネス")
party2.introduction("ルーネス") # オーバーライドをしない場合は継承元クラスのメソッドが呼び出される
party3.introduction("ルーネス") # オーバーライドしているため、魔導士クラスのメソッドが呼び出される
party4.introduction("ルーネス") # オーバーライドしているため、忍者クラスのメソッドが呼び出される

上のプログラムを実行すると、以下の実行結果が出てきます。

私の名前はアルク。よろしくねルーネスさん。
私の名前はケン。よろしくねルーネスさん。
私の名前はレフィアです。ルーネスさん、これからよろしくお願いします!
拙者、イングスと申す。ルーネス殿、よろしくでござる。

Monk クラスはオーバーライドをしていないため、継承元の Player クラスのメソッドが呼び出されています。

一方 Mage クラスと Ninja クラス内では introduction メソッドを定義し直しているため、オーバーライドが起こり、自分自身のクラスのメソッドが呼び出されていることがわかりますね。

6. ポリモフィズム(多様性・多相性)

(1) ポリモフィズムとは

同じメッセージ*9を(継承を利用して定義された)複数のオブジェクトに送っても、それぞれのオブジェクトごとに動作が異なる性質のことをポリモフィズムと呼びます。多様性、多相性とも呼ばれます。


とは言ってもわかりにくいので1つ例を用意しましょう。

「うさぎさん」、「あざらしさん」、「ねこさん」の3人(?)がいたとします。

この3人に先生が「勉強せよ!」と命令したとします(メッセージを送る)。

このとき、3人は「勉強せよ!」と同じことを言われていますが、勉強する内容、つまり動作はそれぞれ異なりますよね。ある人は国語を勉強するかもしれませんし、ある人は数学を勉強するかもしれません。

f:id:momoyama1192:20200712024412g:plain

このように、同じことを言っても受け取った人(オブジェクト)ごとに異なる動作をするのがポリモフィズムなのです。

(2) Pythonで確認

ポリモフィズムはオーバーライドを行うことで実現できます。

ということで、実際にオーバーライドを行いポリモフィズムがどんなものなのかを体感しましょう。


※プログラムはオーバーライドの確認のときとほとんど同じです

# クラス「冒険者(プレイヤー)」
class Player():
    def __init__(self,name):
        self.name      = name    # 名前
    # 自己紹介関数、引数 opponent には相手の名前を入れる
    def introduction(self,opponent):
        print("私の名前は" + self.name + "。よろしくね" + opponent + "さん。")

# クラス「魔道士」(Playerを継承)
class Mage(Player): 
    def introduction(self,opponent): # 自己紹介関数をオーバーライド
        print("私の名前は" + self.name + "です。" + opponent + "さん、これからよろしくお願いします!")

# クラス「忍者」(Playerを継承)
class Ninja(Player):
    def introduction(self,opponent): # 自己紹介関数をオーバーライド
        print("拙者、" + self.name + "と申す。" + opponent + "殿、よろしくでござる。")



party1 = Player("アルク")   # 冒険者クラス
party2 = Mage("レフィア")   # 魔道士クラス
party3 = Ninja("イングス")  # 忍者クラス

# 同じ introduction でも3つの動作が微妙に違う、これがポリモフィズム!
party1.introduction("ルーネス")
party2.introduction("ルーネス") 
party3.introduction("ルーネス") 

このプログラムを実行すると…

私の名前はアルク。よろしくねルーネスさん。
私の名前はレフィアです。ルーネスさん、これからよろしくお願いします!
拙者、イングスと申す。ルーネス殿、よろしくでござる。

同じ introduction メソッドを使っているのに3つの実行結果が全然違いますよね。

まさにこれがポリモフィズムなのです!

7. 練習問題

では、オブジェクト指向について復習するために基本情報の問題を解いてみましょう。

練習1

オブジェクト指向の基本概念の組合せとして適切なものはどれか。

[基本情報技術者平成19年秋期 午前問44]

ア:仮想化、構造化、投影、クラス
イ:具体化、構造化、連続、クラス
ウ:正規化、カプセル化、分割、クラス
エ:抽象化、カプセル化、継承、クラス

練習2

オブジェクト指向の特徴はどれか。

[基本情報技術者平成23年特別 午前問48]

ア:オブジェクト指向では、抽象化の対象となるオブジェクトに対する操作をあらかじめ指定しなければならない。

イ:カプセル化によって、オブジェクト間の相互依存性を高めることができる。

ウ:クラスの変更を行う場合には、そのクラスの上位にあるすべてのクラスの変更が必要となる。

エ:継承という概念によって、モデルの拡張や変更の際に変更箇所を局所化できる。

練習3

オブジェクト指向の考え方に基づくとき、一般的に“自動車”のサブクラスといえるものはどれか。

[基本情報技術者平成18年秋期 午前問40]

ア:エンジン
イ:製造番号
ウ:タイヤ
エ:トラック

練習4

オブジェクト指向におけるカプセル化を説明したものはどれか。

[基本情報技術者平成19年春期 午前問42]

ア:同じ性質をもつ複数のオブジェクトを抽象化して、整理すること

イ:基底クラスの性質を派生クラスに受け継がせること

ウ:クラス間に共通する性質を抽出し、基底クラスを作ること

エ:データとそれを操作する手続を一つにして、オブジェクトの内部に隠ぺいすること

練習5

オブジェクト指向プログラミングにおける、多相性を実現するためのオーバーライドの説明はどれか。

[基本情報技術者平成29年秋期 午前問7]

ア:オブジェクト内の詳細な仕様や構造を外部から隠蔽すること

イ:スーパークラスで定義されたメソッドをサブクラスで再定義すること

ウ:同一クラス内に、メソッド名が同一で、引数の型、個数、並び順が異なる複数のメソッドを定義すること

エ:複数のクラスの共通する性質をまとめて、抽象化したクラスを作ること

8. 練習問題の答え

解答1

解答:エ

オブジェクト指向に出てくる単語をまとめておいたのでこの際全部頭にいれておきましょう。

  • 抽象化(オブジェクトを抽象化してクラスを定義)
  • カプセル化(外部から直接データにアクセスできなくする)
  • 継承(スーパークラスの特性をサブクラスに引き継ぐ)
  • 汎化(サブクラスをまとめてスーパークラスを定義)
  • 特化(スーパークラスの具体例としてサブクラスを定義)
  • 集約(単純なクラスを集め合わせて1つの複雑なクラスにすること)
  • 分解(1つの複雑なクラスをいくつかの単純なクラスにバラバラにすること)
  • オーバーライド(スーパークラスで継承したメソッドをサブクラスで定義して上書きすること)
  • ポリモフィズム(同じメッセージを送ってもオブジェクトごとに動作内容が変わること)

解答2

解答:エ

それぞれの選択肢を見ていきましょう。

ア:オブジェクト指向では、抽象化の対象となるオブジェクトに対する操作をあらかじめ指定しなければならない。
→ あらかじめ指定する必要はありません(例えば抽象化したもの(インスタンス)を定義せずに継承のためだけに使うようなクラス(抽象クラス)だとオブジェクトを指定せずに使うことがあります)。

イ:カプセル化によって、オブジェクト間の相互依存性を高めることができる。
→ カプセル化はオブジェクト内にデータと手続きをまとめることなので、オブジェクト間の相互依存性は低くなる。

ウ:クラスの変更を行う場合には、そのクラスの上位にあるすべてのクラスの変更が必要となる。
→ 下位クラス独自の内容を変更する場合は上位クラスを変更する必要はありません。

エ:継承という概念によって、モデルの拡張や変更の際に変更箇所を局所化できる。
→ 正しい

解答3

解答:エ

自動車のサブクラスということは、自動車の具体例になるものを答えればよい。

トラックは自動車の具体例(トラック is a 自転車とis-a関係が成り立つ)ので答えはエ。

ちなみに残りの3つの選択肢(エンジン、製造番号、タイヤ)にはすべてpart-of関係が成り立ちます。

解答4

解答:エ

それぞれの選択肢を見ていきましょう。

ア:同じ性質をもつ複数のオブジェクトを抽象化して、整理すること
→ 抽象化のこと。(特化のことかも。)

イ:基底クラスの性質を派生クラスに受け継がせること
→ 継承(インヘリタンス)のこと。

ウ:クラス間に共通する性質を抽出し、基底クラスを作ること
→ 汎化のこと。

エ:データとそれを操作する手続を一つにして、オブジェクトの内部に隠ぺいすること
→ 正解。これがカプセル化。

解答5

解答:イ

それぞれの選択肢を見ていきましょう。

ア:オブジェクト内の詳細な仕様や構造を外部から隠蔽すること
→ カプセル化のこと。

イ:スーパークラスで定義されたメソッドをサブクラスで再定義すること
→ オーバーライドのこと。正解。

ウ:同一クラス内に、メソッド名が同一で、引数の型、個数、並び順が異なる複数のメソッドを定義すること
→ オーバーロードのこと。オーバーロードとオーバーライドはごちゃごちゃになりやすいので要注意!

エ:複数のクラスの共通する性質をまとめて、抽象化したクラスを作ること
→ 汎化のこと。

9. さいごに

今回は、オブジェクト指向とはどんなものなのかをPythonを使いながら説明していきました。


次回は、オブジェクト指向言語の設計で使われる標準的な記法であるUMLについて説明していきましょう。

*1:現代のソフトウェア、システム開発では「複数の要素が絡み合った大規模で複雑なプログラム」を扱うことがほとんどで、なおかつ「デバッグやメンテナンス」は定期的に行われるので手続き型よりもオブジェクト指向なプログラミング手法が使われることがほとんど(だと思うの)です。

*2:つまり外部からの不正アクセスを防止することができるってことです。

*3:具体的なデータの中身、処理内容などを

*4:入力と出力が一緒であれば

*5:Javaでいう privateで宣言する操作に相当します。

*6:外部からアクセスしないメソッドなどに使うことで、外部から勝手にアクセスすることを防げます。

*7:余談ですが、スーパークラスにない不足したデータ、メソッドだけに注目してオブジェクトを定義する方法のことを差分プログラミングと呼ばれます。

*8:ちなみにオーバーロードはPythonには実装されていません。

*9:厳密にいうと違うのですが、同じメッセージを送るというのは「同じ名前のメソッド(関数)」を呼び出すという意味だと思ってください。