LoginSignup
5
3

More than 3 years have passed since last update.

オブジェクト指向について説明してみる

Last updated at Posted at 2019-09-05

背景

仕事でPythonで書かれたプログラムを扱っているが、検証や仕様修正などを考えるにあたって仕事を分担したい。
ただ、「オブジェクト指向とは何ぞ??」という人が結構いるので、そういう人たちに向けた説明を書き残しておこうと思った。(筆者も独学なので怪しいけど…)

対象

  • if/for/whileとかくらいのプログラムの知識はある(C言語とか多少触ったことある)
  • Python3系の書式をなんとなく知っている or 知らないけどわかんなかったら調べる気概がある

執筆時の環境

  • OS:Windows10
  • Python: 3.7.3

本記事のサンプルソース

本記事に登場するサンプルソースは、GitHubに置いてある。
実行まで試したい人は、ここからどうぞ。

オブジェクト指向とは

定義的なものはこのような感じらしい。
出典:Wikipedia

オブジェクト指向(object-oriented)の言葉を生み出した計算機科学者アラン・ケイは、1970年代に発表した文書の中でその設計構想を六つの要約で説明している[1]。

1, EverythingIsAnObject.
2, Objects communicate by sending and receiving messages (in terms of objects).
3, Objects have their own memory (in terms of objects).
4, Every object is an instance of a class (which must be an object).
5, The class holds the shared behavior for its instances (in the form of objects in a program list).
6, To eval a program list, control is passed to the first object and the remainder is treated as its message.
— Alan Kay

以下はその和訳である。

1.すべてはオブジェクトである。
2.オブジェクトはメッセージの受け答えによってコミュニケーションする。
3.オブジェクトは自身のメモリーを持つ。
4.どのオブジェクトもクラスのインスタンスであり、クラスもまたオブジェクトである。
5.クラスはその全インスタンスの為の共有動作を持つ。インスタンスはプログラムにおけるオブジェクトの形態である。
6.プログラム実行時は、制御は最初のオブジェクトに渡され、残りはそのメッセージとして扱われる。

とりあえず、何でもかんでもオブジェクトとして定義して、オブジェクト同士のやり取りで成り立つよという感じ。
こんなイメージ。
概要イメージ.png

「ふ~ん」くらいで見てれば大丈夫だと思う。

クラス

何でもオブジェクトとして扱う為には、「そのオブジェクトはどんなものか?」ということをハッキリさせるための定義が必要になる。
といってもわかりにくいので、"ネコ"のクラスを作る例で考えてみる。
(ネコ好きなんです...)

プロパティ

まず、一言にネコといっても、世界には色んな種類がいるし、さらに"毛色”や"目の色”、"性別"、"長毛か短毛か"など、ちょっと考えただけで沢山の項目が思い浮かぶ。
ネコの要素.png
このように、"そのオブジェクト"を示す特徴になる固有の情報を示す項目をプロパティ(属性)という。
上で出した例からすると、ネコのプロパティは…

  • 種類
  • 毛色
  • 目の色
  • 毛種
  • 性別

と定義できる。図鑑とかちゃんと見ればもっと色々出てくるだろうが、"プロパティに何を持たせるか"はそのオブジェクトを使うプログラムの処理で必要かどうか?で決まる。

例えば、猫カフェでタブレットを設置しておいて、"ネコの名前をタップしたら、年齢、どんな種類か?、好物は何か?"などの情報を表示してくれるアプリを作成するとした場合、上の例で定義したプロパティだけでは"名前"を選んでもどのネコの情報なのか検索しようとしてもできないし、"年齢"、"好物"という情報を取得しようとしてもプロパティが存在していないので、プロパティの定義が不十分であるということになる。
ネコプロパティ例.png
なので、このアプリを実現しようと思うと、プロパティに次の情報を追加することになる。

  • 名前←追加
  • 好物←追加
  • 年齢←追加
  • 種類
  • 毛色
  • 目の色
  • 毛種
  • 性別

上の例では、「最初にネコといえば・・・」で思いつく項目をプロパティとして最初に登録したが、実際には"ネコ"のオブジェクトが必要というところが間違っていなければ、詳細を詰めるときに追加していけばよい(と思う)。

Pythonで実装してみる

ここまで検討した"ネコ”クラスをpythonで実装してみた。
以後の説明でも使っていきたいので、ディレクトリ構成は次のようになっているという前提で。

root/
  └src/
    └cat.py
cat.py
class Cat:
    """
    ネコのクラス
    """
    def __init__(
        self,
        name, age, sex, type_name, favorite_food,
        fur_color, eye_color, fur_type
    ):
        # ネコのプロパティ
        self.name = name
        self.age = age
        self.sex = sex
        self.type_name = type_name
        self.favorite_food = favorite_food
        self.fur_color = fur_color
        self.eye_color = eye_color
        self.fur_type = fur_type

とりあえずクラス名はそのまんまCatとした。
プロパティを定義しているのはself.name = ...以降の部分で、def __init__(...の部分はもう少し後で説明するので、時点ではおまじない的に書いてあると思ってくれればいい。

インスタンス

とりあえずプロパティを定義したCatクラスができたわけだが、クラスはあくまで"定義"なので、それ自体は実体を持っていない。
プログラム内で使うには、Catクラスの定義に沿った実体をメモリに展開(=実態を作る)して初めて使うことができる。
この実体のことを"インスタンス"と呼ぶ。

試しに、先ほどのcat.pyど同一ディレクトリ下にmain.pyを作ってみる。

root/
  └src/
    ├cat.py
    └main.py
main.py
from cat import Cat

# jiji = Cat("ジジ", 13, "オス", "ニシンパイ", "黒", "白", "短毛種")
jiji = Cat("ジジ", 13, "オス", "", "ニシンパイ", "黒", "白", "短毛種")  # 19'9/
print(jiji.name)
print(jiji.sex)
print(jiji.age)

root下でpython .\src\main.pyを実行すると次のように出力が出てくるはず。

> python .\src\main.py
ジジ
オス
13

jiji = Cat("ジジ",...で、"ジジ"というネコのオブジェクトの実体をjijiに作ることができた。
インスタンスができたことで、jijiにアクセスすればメモリ上にある"ジジ"の情報を取れるようになったので、print(...の部分で"名前"、"性別"や"年齢"を取得して表示できている。
上のサンプルでは"名前","性別","年齢"しか取得していないが、もちろん"毛色"などのCatクラスで定義した他のプロパティも取得できる。

メソッド

ここまで、Catクラスを定義してインスタンスを作って、情報を取得して表示ということをやってみたが、正直これでは何が便利なのかよくわからないと思う。
C言語に馴染みがある人なら、「構造体でも同じじゃん」と思うだろう。
実際、"プロパティを定義して個体毎に値を管理する"だけなら構造体と差異はない。
クラスが本領を発揮するのは、プロパティに加えて"オブジェクトの振る舞い"も一緒に定義できるところにある。

ここで、Catクラスに定義した"年齢"というプロパティについて考えてみると、各ネコ毎に「何歳か?」を登録するわけだが、ネコは生き物なので月日とともに歳をとる。当然、毎年更新する必要が出てくる。
素直に考えると、"登録済みのネコが誕生日を迎える度に猫カフェのスタッフが手動で書き換える"という手であるが、もう少し考えてみれば年齢というのは誕生日さえ分かればどのネコでも同じように求められるということに気づく(ネコというか動物全般だが)。
年齢算出.png
このように、そのオブジェクトが持つ固有の振る舞い定義したものを"メソッド"という。

"固有の振る舞いの定義"というと難しく聞こえるが、要するに"そのオブジェクト専用の関数"である。
実際に誕生日から年齢を求める"年齢算出メソッド"を考えてみる。
まずは、"ネコ"クラスに誕生日のプロパティを追加する必要がある。

  • 誕生日←追加
  • 名前
  • 好物
  • 年齢
  • 種類
  • 毛色
  • 目の色
  • 毛種
  • 性別

誕生日プロパティが追加できたら、メソッドの設計に入る。
今回の機能は、次のようなフローで実現できるはず。
年齢取得フロー.png
実際にCatクラスを改造してみよう。

cat.py
from datetime import date

class Cat:
    """
    ネコのクラス
    """
    def __init__(
        self,
        name, birth_day, sex, type_name, favorite_food,
        fur_color, eye_color, fur_type
    ):
        # ネコのプロパティ
        self.name = name
        self.birth_day = birth_day # 誕生日のプロパティを追加
        self.age = None
        self.calculation_age()     # 誕生日から自動算出
        self.sex = sex
        self.type_name = type_name
        self.favorite_food = favorite_food
        self.fur_color = fur_color
        self.eye_color = eye_color
        self.fur_type = fur_type

    def calculation_age(self):
        """
        誕生日から年齢を算出する
        """
        # 今日の日付を取得
        today = date.today()
        # 去年の年齢を取得
        age = today.year - self.birth_day.year - 1
        # 今年の誕生日を迎えていれば+1歳
        # if today.month >= self.birth_day.month:
        #     if today.day >= self.birth_day.day:
        #         age += 1
        # 19'9/8 修正
        if today.month > self.birth_day.month:
            age += 1
        elif today.month == self.birth_day.month:
            if today.day >= self.birth_day.day:
                age += 1
        # プロパティに反映
        self.age = age

クラスの改造に合わせてmain.pyも少し修正して実行してみる。

main.py
from datetime import date

from cat import Cat

# jiji = Cat("ジジ", 13, "オス", "", "ニシンパイ", "黒", "白", "短毛種")
jiji = Cat("ジジ", date(2017, 5, 10), "オス", "", "ニシンパイ", "黒", "白", "短毛種")
print(jiji.name)
# print(jiji.sex)
print(jiji.birth_day)
print(jiji.age)
> python .\src\main.py
ジジ
2017-05-10
2

jiji = Cat(...で"年齢"ではなく"誕生日"でインスタンスを作っているのに、print(jiji.age)で2歳と取得できたと思う(本記事執筆は2019年09月)。

これを実現しているのが、

cat.py
def Cat:
    # ~省略
    def calculation_age(self):
        # 省略

の部分で"誕生日"から"年齢"を設定する処理を定義しているメソッドになっていて、

cat.py
def Cat:
    def __init__(
        # ~省略
        self.calculation_age()     # 誕生日から自動算出
        # ~省略

の部分でインスタンス生成時にcalculation_age()メソッドを実行して"年齢"を取得している。

実はプロパティの説明時点から登場していたが、クラス内では自分自身を"self"という名称で参照できるようになっている。
なので、calculation_age()メソッド内で"birth_day"や"age"のプロパティ(インスタンス変数)を読み書きできているし、self.calculation_age()という形でメソッドを呼べている。

ちなみに、Pythonの場合はメソッド定義で第一引数にselfを与えないと、メソッド内部で参照した時点でエラーになるが、このあたりの書式仕様は言語によって異なるみたい。

コンストラクタと特殊メソッド

ここまで、Catクラスを作ってきたが、def __init__(...という部分を「おまじない的に」と前述していた。
"メソッド"を知った後だと、__init__()が同じ書き方であることに気づく。
実際、この__init__()も立派な"メソッド"なのだが、インスタンス生成時に自動的に呼ばれるものと決まっているという特徴がある。
この振る舞いをするメソッドは、コンストラクタと呼ばれ、Pythonでは__init__というメソッド名で識別している。
コンストラクタ識別のルールは言語仕様によって違うので、java コンストラクタc++ コンストラクタという具合に知りたい言語に合わせて調べるとよい。

コンストラクタのような、言語仕様的に予め振る舞いが決められているメソッドを、特殊メソッドといって、コンストラクタ以外にも色々なものがある。
ここでは、代表例として、文字列変換の特殊メソッド__str__()Catクラスに実装してみる。

"文字列変換の特殊メソッド"とは何かというと、この時点のCatクラスを直接print関数で表示してみると、

main.py
from datetime import date

from cat import Cat

jiji = Cat("ジジ", date(2017, 5, 10), "オス", "", "ニシンパイ", "黒", "白", "短毛種")
print(jiji)
> python .\src\main.py
<cat.Cat object at 0x000002C33FB479E8>

こんな風に、オブジェクトのIDが出力される。

次は、Catクラスに次のような__str__メソッドを追加して、同じmain.pyを実行してみる。

cat.py
from datetime import date

class Cat:
    """
    ネコのクラス
    """
    def __init__(
        # ~省略~

    def __str__(self):
        delimiter = '\t'
        linefeed = '\n'
        ret_str = "名前" + delimiter
        ret_str += self.name + linefeed
        ret_str += "性別" + delimiter
        ret_str += self.sex + linefeed
        ret_str += "誕生日" + delimiter
        ret_str += self.birth_day.strftime("%Y/%m/%d") + linefeed
        ret_str += "年齢" + delimiter
        ret_str += str(self.age) + linefeed
        ret_str += "種類" + delimiter
        ret_str += self.type_name + linefeed
        ret_str += "毛色" + delimiter
        ret_str += self.fur_color + linefeed
        ret_str += "目の色" + delimiter
        ret_str += self.eye_color + linefeed
        ret_str += "毛種" + delimiter
        ret_str += self.fur_type + linefeed
        ret_str += "好物" + delimiter
        ret_str += self.favorite_food
        return ret_str

    def calculation_age(self):
        # ~省略~
> python .\src\main.py
名前    ジジ
性別    オス
誕生日  2017/05/10
年齢    2
種類
毛色    黒
目の色  白
毛種    短毛種
好物    ニシンパイ

main.pyは変えていないのに、今度はネコのプロパティ一覧が出力されるようになった。

このように、"特殊メソッド"というのは明示的に呼び出さなくても、"既定の条件で自動的に呼び出されるメソッド"で、知っていると便利なことが多い。(汎用的な条件だからこそ既定できるわけで…)
どんな特殊メソッドがあるか知らないが故に、「普通のメソッド定義して、呼出し用の条件分も作って・・・」とやっていたら"実は2度手間だった!"みたいなことが起きえるので、一度調べて頭の片隅に残しておくといいだろう。

クラス図(参考)

参考程度だが、クラスの定義を抽象的に伝えられるようにクラス図というものがある。
ここまでに作ったCatクラスをクラス図で書くと次のようになる。
2019-09-05_03-18_cat_class.png
一番上はクラス名で、その下にプロパティ一覧が並び、さらに下にメソッド一覧が並ぶ。
今回は日本語で書いてみたが、実装とのリンク性を強めたければ、ソース上のプロパティ名やメソッド名をそのまま書くべきだし、逆にあまり詳しくない人に抽象的に説明する場合は、日本語に読み替えたりしたほうがわかりやすい。

アプリにしてみる

クラスの最後にプロパティの項で例に出した、"猫カフェで名前を選んだら情報を表示するアプリ"を作ってみる。
ここではタブレットの代わりにPCのアプリとして、"ドロップダウンリストから選んで、ボタンをクリック"という風にする。

cat.py
# coding: utf-8
from datetime import date

class Cat:
    """
    ネコのクラス

    params
        name: (str)名前
        birth_day: (datetime.date)誕生日
        sex: (str)性別
        type_name: (str)種類
        favorite_food: (str)好物
        fur_color: (str)毛色
        eye_color: (str)目の色
        fur_type: (str)毛種
    """
    def __init__(
        self,
        name, birth_day, sex, type_name, favorite_food,
        fur_color, eye_color, fur_type
    ):
        # ネコのプロパティ
        self.name = name
        self.birth_day = birth_day
        self.age = None
        self.calculation_age()
        self.sex = sex
        self.type_name = type_name
        self.favorite_food = favorite_food
        self.fur_color = fur_color
        self.eye_color = eye_color
        self.fur_type = fur_type

    def __str__(self):
        delimiter = '\t'
        linefeed = '\n'
        ret_str = "名前" + delimiter
        ret_str += self.name + linefeed
        ret_str += "性別" + delimiter
        ret_str += self.sex + linefeed
        ret_str += "誕生日" + delimiter
        ret_str += self.birth_day.strftime("%Y/%m/%d") + linefeed
        ret_str += "年齢" + delimiter
        ret_str += str(self.age) + linefeed
        ret_str += "種類" + delimiter
        ret_str += self.type_name + linefeed
        ret_str += "毛色" + delimiter
        ret_str += self.fur_color + linefeed
        ret_str += "目の色" + delimiter
        ret_str += self.eye_color + linefeed
        ret_str += "毛種" + delimiter
        ret_str += self.fur_type + linefeed
        ret_str += "好物" + delimiter
        ret_str += self.favorite_food
        return ret_str

    def calculation_age(self):
        """
        誕生日から年齢を算出する
        """
        # 今日の日付を取得
        today = date.today()
        # 去年の年齢を取得
        age = today.year - self.birth_day.year - 1
        # 今年の誕生日を迎えていれば+1歳
        if today.month > self.birth_day.month:
            age += 1
        elif today.month == self.birth_day.month:
            if today.day >= self.birth_day.day:
                age += 1
        # プロパティに反映
        self.age = age

    def to_dict(self):
        """
        各プロパティを辞書化する

        param
            None
        return
            (dict)作成した辞書(key:属性名, val:属性値)
        """
        ret_dict = {
            "名前": self.name,
            "性別": self.sex,
            "誕生日": self.birth_day,
            "年齢": self.age,
            "種類": self.type_name,
            "毛色": self.fur_color,
            "目の色": self.eye_color,
            "毛種": self.fur_type,
            "好物": self.favorite_food
        }
        return ret_dict


class CatInquirer:
    """
    ネコの情報を紹介する

    param
        cats: (list)Catオブジェクトのリスト
    """
    def __init__(self, cats=[]):
        # 名前をキーにした辞書に変換
        self.cats = {x.name: x for x in cats}

    def increase(
        self,
        name, birth_day, sex, type_name, favorite_food,
        fur_color, eye_color, fur_type, force=False
    ):
        """
        Catオブジェクトを直接生成して追加する

        param
            force: (bool)同名のネコがいる場合、強制的に情報の上書きをするか
        return
            (Cat)追加したCatインスタンス
        """
        # 既存の確認
        if name in self.cats.keys():
            if not force:
                print(
                    "既に同名のネコがいます。\n" +
                    "情報を上書きする場合は\"force\"オプションを" +
                    "Trueにして再試行して下さい。"
                )
                return None
        # 追加するCatインスタンスを作成
        tmp_cat =  Cat(
            name, birth_day, sex, type_name, favorite_food,
            fur_color, eye_color, fur_type
        )
        # 追加
        self.cats[tmp_cat.name] = tmp_cat
        return tmp_cat

    def find_cat(self, name):
        """
        指定した名前のCatインスタンスを取得して返す

        param
            name: (str)探索するネコの名前
        return
            (Cat)見つかったCatのインスタンス
        """
        if name not in self.cats.keys():
            raise KeyError("そんなネコはいないよ")
        return self.cats[name]

    def get_names(self):
        """
        登録済みのネコたちの名前一覧を返す

        param
            None
        return
            (list)ネコの名前リスト
        """
        return list(self.cats.keys())

    def to_dict(self, name):
        """
        指定した名前のCatインスタンスに対応する辞書を返す

        param
            name: (str)探索するネコの名前
        return
            (dict)見つかったCatのインスタンスの辞書(key:属性名, val:属性値)
        """
        return self.find_cat(name).to_dict()

Catクラス自体は、プロパティを辞書で返すto_dict()メソッドを追加しただけだが、新たにCatInquirerというクラスを定義している。
このクラスは、複数のCatインスタンスをプロパティに持っておいて、情報の照合を行う役割を持っている。

今回は、"複数匹のネコの情報から1つを選んで表示する"という機能なので、選択肢の提供と選択結果との照合をするために作成した。

main.py
# coding: utf-8
import tkinter
from tkinter import ttk
from datetime import date

from cat import Cat
from cat import CatInquirer


class OperationWindow(tkinter.Frame):
    """
    GUIの操作画面クラス
    """
    def __init__(self, master=None, values=[], command=None):
        # フレームを作成
        super().__init__(master=master)
        self.pack()

        # コールバック関数の設定
        if command is None:
            command = self.dmy_command
        self.command = command

        # 指定リストのコンボボックスを作成
        self.value = tkinter.StringVar()
        self.name_box = ttk.Combobox(
            master=self, state="readonly", values=values
        )
        self.name_box.current(0)
        self.name_box.pack(padx=5, pady=5)

        # 選択を確定するボタンを作成
        self.select_btn = ttk.Button(
            master=self, text="Select", command=self.decition_name
        )
        self.select_btn.pack(padx=5, pady=5)

    def dmy_command(self, *args):
        """
        コールバック未指定時のダミー関数
        """
        return {}

    def decition_name(self):
        """
        選択確定ボタンを押した時の処理
        """
        # コンボボックスの現在表示値を取得
        self.value.set(self.name_box.get())
        # 対応する表示情報の辞書を取得
        infos = self.command(self.value.get())
        self.show_information(infos)

    def show_information(self, infos):
        """
        情報表示のサブウィンドウを開く

        param
            infos: (dict)表示する情報の辞書(key: 項目名, val: 情報の内容)
        return
            None
        """
        # 情報表示用テーブル作成
        sub_win = tkinter.Toplevel()
        information = ttk.Treeview(
            master=sub_win, columns=("値"), show="tree",
            height=len(infos.items())
        )
        for key, val in infos.items():
            information.insert("", "end", text=key, values=(val))
        information.pack()
        # 閉じるボタン作成
        close_btn = tkinter.Button(
            master=sub_win, text="閉じる", command=sub_win.destroy
        )
        close_btn.pack(pady=10)


def main():
    # サンプルとするネコのインスタンス生成
    cats = CatInquirer()
    cats.increase(
        "ジジ", date(2017, 5, 10), "オス", "",
        "ニシンパイ", "黒", "白", "短毛種"
    )
    cats.increase(
        "タマ", date(2015, 11, 23), "オス", "",
        "サンマ", "白", "白", "短毛種"
    )
    cats.increase(
        "ニャース", date(2017, 1, 31), "オス", "ポケモン",
        "がんもどき", "アイボリー", "白", "短毛種"
    )

    # GUIの準備・設定
    root = tkinter.Tk()
    root.title("Cat's Infomations")
    app = OperationWindow(root, cats.get_names(), cats.to_dict)
    # 操作画面の表示
    app.mainloop()


if __name__ == "__main__":
    main()

こちらは、OperationWindowというクラスが新設されている。
これは、与える"選択肢"のコンボボックスと選択肢が決定した時点で別ウィンドウで情報一覧を表示するもので、一応は今回用に作ったが汎用的なクラスである。

これでmain.pyを実行すると、次のような小さなウィンドウが表示されるはず。
image.png

コンボボックスをクリックすると、ドロップダウンリストが出てくる。
image.png
ここに出てくる選択肢は、

app = OperationWindow(root, cats.get_names(), cats.to_dict)

の第2引数で渡したリストの中身が設定されている。

好きなネコを選んで、selectボタンを押すと別のウィンドウが開いて、次のような情報が表示される。
image.png
ここで表示する情報は、

main.py
app = OperationWindow(root, cats.get_names(), cats.to_dict)

の第3引数で指定した関数の戻り値(dictである必要がある)になっていて、今回のcats.to_dictだと、

cat.py
# ~省略~

def find_cat(self, name):
    """
    指定した名前のCatインスタンスを取得して返

    param
        name: (str)探索するネコの
    return
        (Cat)見つかったCatのインスタ
    """
    if name not in self.ca
        raise KeyError("そんなネコはいないよ")
    return self.cats[name]

# ~省略~

def to_dict(self, name):
    """
    指定した名前のCatインスタンスに対応する辞書を返す

    param
        name: (str)探索するネコの名前
    return
        (dict)見つかったCatのインスタンスの辞書(key:属性名, val:属性値)
    """
    return self.find_cat(name).to_dict()

"nameで渡された名前のネコのインスタンスを取得して、to_dict()メソッドの結果を返す"ということをしている。
nameはコンボボックスの表示状態を取得するようになっているので、OperationWindowに渡す引数を2つともcatsから設定することで矛盾なく情報を取れるようになっている。

継承という概念

ここまで"ネコ"クラスを作ったが、このアプリを"猫カフェ"から"動物園"に適用拡大しようと考えてみる。
せっかく作ったCatクラスをAnimalクラスとかにリネームして使い、いろんな動物を登録したいところだが、動物によって特徴が出る部分は全然違う。

例えば、"ゾウ"はほとんど毛がなく、見た目の色を表すならば"毛色"より"体色"というべきだし、"目の色"もみんな黒で見た目の特徴とは言えない。
逆に"牙の長さ"や"耳の形"なんかは、ゾウの見た目の特徴になってくる。
じゃあ、「"牙の長さ"というプロパティを追加すればいいじゃん」と一瞬よぎるが、今度は"パンダ"の特徴は?、"クジャク"は?、"ウサギ"は?、"ゴリラ"は?...と考えては足りないプロパティをどんどん追加していくと、途端にAnimalクラスは肥大化し、その割に各インスタンス単位で見るとほとんどのプロパティを使わないという状態になる。
動物園 (1).png

そうすると今度は「Catクラスをコピーして、それぞれの動物のクラスを作れば解決じゃん」という考えがよぎる。
実際、それぞれの動物別にクラスを作れば、プロパティは"その動物のものだけ"を定義することになるので、考え方としては間違っていない。
一方で、動物園で飼っている以上、"名前"、"誕生日"、"年齢"というプロパティはどんな動物のクラスでも定義するものだというのが想像できるし、当然、年齢算出メソッドも動物によって処理が変化するものではないことがわかる。
動物園 (2).png

共通部分があろうがなかろうが、コピペすれば特に問題はなさそうに思えるが、保守・メンテ性としては最悪なことになる。
例えば、Catクラスで作った"年齢算出メソッド"だが、実はうるう年を考慮できていない。
このバグに気づいたときに、既にCatクラスをコピーして数十種類の動物のクラスを作ってしまっていたらどうだろうか?きっと絶望感は想像に難しくないだろう。

この問題を解決するのが、"継承"という概念になる。
その名の通り、共通する部分のみを定義したクラス(基底クラスという)を作って、他のクラスに"継承"させる。
"継承"されて作られたクラス(サブクラスという)は、"基底クラス"のメソッドやプロパティも使える状態で、各々のクラス専用のプロパティやメソッドを追加定義することができる。
2019-09-05_03-18_cat_class-4.png

継承を実装してみる

実際にアプリにしてみるで作ったスクリプトを改造して、"ネコ"以外に"ぞう"も扱えるようにしてみよう。

手始めに、cat.pyanimal.pyにリネームしておこう。
つい面倒くさがってしまいがちだが、ファイル名が実体と合わなくなると後でごちゃつくので、ちゃんと変える癖をつけておいたほうがいい。

root/
  └src/
    ├animal.py
    └main.py

とりあえず、継承を使って作ったソース全体は次のようになった。

animal.py
# coding: utf-8
from datetime import date

class Animal:
    """
    動物のクラス

    params
        name: (str)名前
        birth_day: (datetime.date)誕生日
        sex: (str)性別
        type_name: (str)種類
        favorite_food: (str)好物
    """
    def __init__(self, name, birth_day, sex, type_name, favorite_food):
        # 動物のプロパティ
        self.name = name
        self.birth_day = birth_day
        self.age = None
        self.calculation_age()
        self.sex = sex
        self.type_name = type_name
        self.favorite_food = favorite_food

    def __str__(self):
        self.delimiter = '\t'
        self.linefeed = '\n'
        ret_str = "名前" + self.delimiter
        ret_str += self.name + self.linefeed
        ret_str += "性別" + self.delimiter
        ret_str += self.sex + self.linefeed
        ret_str += "誕生日" + self.delimiter
        ret_str += self.birth_day.strftime("%Y/%m/%d") + self.linefeed
        ret_str += "年齢" + self.delimiter
        ret_str += str(self.age) + self.linefeed
        ret_str += "種類" + self.delimiter
        ret_str += self.type_name + self.linefeed
        ret_str += "好物" + self.delimiter
        ret_str += self.favorite_food
        return ret_str

    def calculation_age(self):
        """
        誕生日から年齢を算出する
        """
        # 今日の日付を取得
        today = date.today()
        # 去年の年齢を取得
        age = today.year - self.birth_day.year - 1
        # 今年の誕生日を迎えていれば+1歳
        if today.month > self.birth_day.month:
            age += 1
        elif today.month == self.birth_day.month:
            if today.day >= self.birth_day.day:
                age += 1
        # プロパティに反映
        self.age = age

    def to_dict(self):
        """
        各プロパティを辞書化する

        param
            None
        return
            (dict)作成した辞書(key:属性名, val:属性値)
        """
        ret_dict = {
            "名前": self.name,
            "性別": self.sex,
            "誕生日": self.birth_day,
            "年齢": self.age,
            "種類": self.type_name,
            "好物": self.favorite_food
        }
        return ret_dict

class Cat(Animal):
    """
    ネコのクラス

    params
        name: (str)名前
        birth_day: (datetime.date)誕生日
        sex: (str)性別
        type_name: (str)種類
        favorite_food: (str)好物
        fur_color: (str)毛色
        eye_color: (str)目の色
        fur_type: (str)毛種
    """
    def __init__(
        self,
        name, birth_day, sex, type_name, favorite_food,
        fur_color, eye_color, fur_type
    ):
        # 動物のプロパティ
        super().__init__(name, birth_day, sex, type_name, favorite_food)
        # ネコのプロパティ
        self.fur_color = fur_color
        self.eye_color = eye_color
        self.fur_type = fur_type

    def __str__(self):
        ret_str = super().__str__()
        ret_str += "毛色" + self.delimiter
        ret_str += self.fur_color + self.linefeed
        ret_str += "目の色" + self.delimiter
        ret_str += self.eye_color + self.linefeed
        ret_str += "毛種" + self.delimiter
        ret_str += self.fur_type + self.linefeed
        return ret_str

    def to_dict(self):
        """
        各プロパティを辞書化する

        param
            None
        return
            (dict)作成した辞書(key:属性名, val:属性値)
        """
        ret_dict = super().to_dict()
        ret_dict["毛色"] = self.fur_color
        ret_dict["目の色"] = self.eye_color
        ret_dict["毛種"] = self.fur_type
        return ret_dict


class Elephant(Animal):
    """
    ぞうのクラス

    params
        name: (str)名前
        birth_day: (datetime.date)誕生日
        sex: (str)性別
        type_name: (str)種類
        favorite_food: (str)好物
        tusk_length: (float)牙の長さ
        ear_form: (str)耳の形
        nose_length: (float)鼻の長さ
    """
    def __init__(
        self,
        name, birth_day, sex, type_name, favorite_food,
        tusk_length, ear_form, nose_length
    ):
        # 動物のプロパティ
        super().__init__(name, birth_day, sex, type_name, favorite_food)
        # ぞうのプロパティ
        self.tusk_length = tusk_length
        self.ear_form = ear_form
        self.nose_length = nose_length

    def __str__(self):
        ret_str = super().__str__()
        ret_str += "牙の長さ" + self.delimiter
        ret_str += self.tusk_length + self.linefeed
        ret_str += "耳の形" + self.delimiter
        ret_str += self.ear_form + self.linefeed
        ret_str += "鼻の長さ" + self.delimiter
        ret_str += self.nose_length + self.linefeed
        return ret_str

    def to_dict(self):
        """
        各プロパティを辞書化する

        param
            None
        return
            (dict)作成した辞書(key:属性名, val:属性値)
        """
        ret_dict = super().to_dict()
        ret_dict["牙の長さ"] = self.tusk_length
        ret_dict["耳の形"] = self.ear_form
        ret_dict["鼻の長さ"] = self.nose_length
        return ret_dict

class AnimalInquirer:
    """
    動物の情報を紹介する

    param
        animals: (list)AnimalまたはAnimalのサブクラスのオブジェクトのリスト
    """
    def __init__(self, animals=[]):
        # 名前をキーにした辞書に変換
        self.animals = {x.name: x for x in animals}

    def is_registered(self, name):
        """
        指定した名前が登録済みかを確認する

        param
            name: (str)確認する名前
        return
            登録済み/True, 未登録/False
        """
        return name in self.animals.keys()

    def can_registered(self, name, force=False):
        """
        指定した名前が登録可能かを確認する

        param
            name: (str)確認する名前
            force: (bool)同名の動物がいる場合、強制的に可能とするか
        return
            登録可/True, 登録不可/False
        """
        if not force and self.is_registered(name):
            print(
                "既に同名の動物がいます。\n" +
                "情報を上書きする場合は\"force\"オプションを" +
                "Trueにして再試行して下さい。"
            )
            return False
        else:
            return True

    def add(self, ani_obj):
        """
        新しいAnimal系オブジェクトを登録する

        param
            ani_obj: (Animal or Animalのサブクラス)登録するインスタンス
        return
            None
        """
        self.animals[ani_obj.name] = ani_obj

    def increase_cat(
        self,
        name, birth_day, sex, type_name, favorite_food,
        fur_color, eye_color, fur_type, force=False
    ):
        """
        Catオブジェクトを直接生成して追加する

        param
            force: (bool)同名のネコがいる場合、強制的に情報の上書きをするか
        return
            (Cat)追加したCatインスタンス
        """
        tmp_cat = None
        # 登録可のみ登録
        if self.can_registered(name, force):
            # 追加するCatインスタンスを作成
            tmp_cat =  Cat(
                name, birth_day, sex, type_name, favorite_food,
                fur_color, eye_color, fur_type
            )
            # 追加
            self.add(tmp_cat)
        return tmp_cat

    def increase_elephant(
        self,
        name, birth_day, sex, type_name, favorite_food,
        tusk_length, ear_form, nose_length, force=False
    ):
        """
        Elephantオブジェクトを直接生成して追加する

        param
            force: (bool)同名の動物がいる場合、強制的に情報の上書きをするか
        return
            (Elephant)追加したElephantインスタンス
        """
        tmp_elephant = None
        # 登録可なら登録
        if self.can_registered(name, force):
            # 追加するElephantインスタンスを作成
            tmp_elephant =  Elephant(
                name, birth_day, sex, type_name, favorite_food,
                tusk_length, ear_form, nose_length
            )
            # 追加
            self.add(tmp_elephant)
        return tmp_elephant

    def find_animal(self, name):
        """
        指定した名前のAnimal系インスタンスを取得して返す

        param
            name: (str)探索する動物の名前
        return
            (Cat)見つかったAnimal系のインスタンス
        """
        if not self.is_registered(name):
            raise KeyError("そんな動物はいないよ")
        return self.animals[name]

    def get_names(self):
        """
        登録済みの動物たちの名前一覧を返す

        param
            None
        return
            (list)動物の名前リスト
        """
        return list(self.animals.keys())

    def to_dict(self, name):
        """
        指定した名前のAnimal系インスタンスに対応する辞書を返す

        param
            name: (str)探索する動物の名前
        return
            (dict)見つかったAnimal系のインスタンスの辞書(key:属性名, val:属性値)
        """
        return self.find_animal(name).to_dict()

今回はAnimalという基底クラスを新設し、今までCatクラス内にあった共通化できるプロパティとメソッドを移管した。
pythonでの継承は、サブクラスをclass クラス名(基底クラス名):として定義すると行われるようになっていて、CatクラスはAnimalクラスを継承したことで、こんなにシンプルになった。

animal.py
class Cat(Animal):
    """
    ネコのクラス

    params
        name: (str)名前
        birth_day: (datetime.date)誕生日
        sex: (str)性別
        type_name: (str)種類
        favorite_food: (str)好物
        fur_color: (str)毛色
        eye_color: (str)目の色
        fur_type: (str)毛種
    """
    def __init__(
        self,
        name, birth_day, sex, type_name, favorite_food,
        fur_color, eye_color, fur_type
    ):
        # 動物のプロパティ
        super().__init__(name, birth_day, sex, type_name, favorite_food)
        # ネコのプロパティ
        self.fur_color = fur_color
        self.eye_color = eye_color
        self.fur_type = fur_type

    def __str__(self):
        ret_str = super().__str__()
        ret_str += "毛色" + self.delimiter
        ret_str += self.fur_color + self.linefeed
        ret_str += "目の色" + self.delimiter
        ret_str += self.eye_color + self.linefeed
        ret_str += "毛種" + self.delimiter
        ret_str += self.fur_type + self.linefeed
        return ret_str

    def to_dict(self):
        """
        各プロパティを辞書化する

        param
            None
        return
            (dict)作成した辞書(key:属性名, val:属性値)
        """
        ret_dict = super().to_dict()
        ret_dict["毛色"] = self.fur_color
        ret_dict["目の色"] = self.eye_color
        ret_dict["毛種"] = self.fur_type
        return ret_dict

Elephantクラスもプロパティが違うだけで、構成はCatクラスと同じ。

animal.py
class Elephant(Animal):
    """
    ぞうのクラス

    params
        name: (str)名前
        birth_day: (datetime.date)誕生日
        sex: (str)性別
        type_name: (str)種類
        favorite_food: (str)好物
        tusk_length: (float)牙の長さ
        ear_form: (str)耳の形
        nose_length: (float)鼻の長さ
    """
    def __init__(
        self,
        name, birth_day, sex, type_name, favorite_food,
        tusk_length, ear_form, nose_length
    ):
        # 動物のプロパティ
        super().__init__(name, birth_day, sex, type_name, favorite_food)
        # ぞうのプロパティ
        self.tusk_length = tusk_length
        self.ear_form = ear_form
        self.nose_length = nose_length

    def __str__(self):
        ret_str = super().__str__()
        ret_str += "牙の長さ" + self.delimiter
        ret_str += self.tusk_length + self.linefeed
        ret_str += "耳の形" + self.delimiter
        ret_str += self.ear_form + self.linefeed
        ret_str += "鼻の長さ" + self.delimiter
        ret_str += self.nose_length + self.linefeed
        return ret_str

    def to_dict(self):
        """
        各プロパティを辞書化する

        param
            None
        return
            (dict)作成した辞書(key:属性名, val:属性値)
        """
        ret_dict = super().to_dict()
        ret_dict["牙の長さ"] = self.tusk_length
        ret_dict["耳の形"] = self.ear_form
        ret_dict["鼻の長さ"] = self.nose_length
        return ret_dict

各所でsuper().という記述があるが、これはサブクラスから基底クラスの要素にアクセスすることを表している。

animal.py
# ~省略~
    def __init__(
        self,
        name, birth_day, sex, type_name, favorite_food,
        tusk_length, ear_form, nose_length
    ):
        # 動物のプロパティ
        super().__init__(name, birth_day, sex, type_name, favorite_food)
        # ~省略~

これはElephantクラスのコンストラクタの冒頭だが、super().__init__(name, birth_day, sex, type_name, favorite_food)で、自身の引数の一部を渡して基底クラスのコンストラクタを呼び出している。
こうすることで、基底クラスと共通部分のプロパティ設定を、基底クラスを直接実体化したのと同じように実行できる。

メソッドのオーバーライド

継承とともによく登場するのが、"メソッドのオーバーライド"だ。
"基底クラス"のメソッドと同名のメソッドを"サブクラス"で定義することで、そのサブクラスにおいてはサブクラス側の定義で実行できるようになる。
2019-09-05_03-18_cat_class-5.png

今回のスクリプトでも使っていて、catクラスのto_dict()メソッドを見てみると、

Catクラス
    def to_dict(self):
        """
        各プロパティを辞書化する

        param
            None
        return
            (dict)作成した辞書(key:属性名, val:属性値)
        """
        ret_dict = super().to_dict()
        ret_dict["毛色"] = self.fur_color
        ret_dict["目の色"] = self.eye_color
        ret_dict["毛種"] = self.fur_type
        return ret_dict

super()は基底クラスの呼出しであったので、基底クラスのAnimalクラスにもto_dict()メソッドがあるようだ。

Animalクラス
    def to_dict(self):
        """
        各プロパティを辞書化する

        param
            None
        return
            (dict)作成した辞書(key:属性名, val:属性値)
        """
        ret_dict = {
            "名前": self.name,
            "性別": self.sex,
            "誕生日": self.birth_day,
            "年齢": self.age,
            "種類": self.type_name,
            "好物": self.favorite_food
        }
        return ret_dict

改造前のCatクラスっぽい形になっている。共通プロパティのみの辞書を作って返している。

このようにすると、CatクラスAnimalクラスのto_dict()を継承しているはずなのだが、to_dict()を呼ぶと自身の中のto_dict()が実行される。
この例では、基底クラスのメソッドを実行した結果に追加処理を施しているが、super()を呼ばなければいけないわけではないので、やろうと思えば全く無関係の処理で上書きすることもできる。

情報照会のクラス(AnimalInquirer)は、Catに特化していた変数名やリテラル文字列を見直しているが、基本的にCatInquirerと差異はない。

実行してみる

実際にぞうの情報が取れるか実行してみよう。
CatInquirerAnimalInquirerにマイナーチェンジしたので、その対応で若干ソースが変化しているが、やっていることは特に変わらない。

main.py
# coding: utf-8
import tkinter
from tkinter import ttk
from datetime import date

from animal import Cat
from animal import Elephant
from animal import AnimalInquirer


class OperationWindow(tkinter.Frame):
    """
    GUIの操作画面クラス
    """
    def __init__(self, master=None, values=[], command=None):
        # フレームを作成
        super().__init__(master=master)
        self.pack()

        # コールバック関数の設定
        if command is None:
            command = self.dmy_command
        self.command = command

        # 指定リストのコンボボックスを作成
        self.value = tkinter.StringVar()
        self.name_box = ttk.Combobox(
            master=self, state="readonly", values=values
        )
        self.name_box.current(0)
        self.name_box.pack(padx=5, pady=5)

        # 選択を確定するボタンを作成
        self.select_btn = ttk.Button(
            master=self, text="Select", command=self.decition_name
        )
        self.select_btn.pack(padx=5, pady=5)

    def dmy_command(self, *args):
        """
        コールバック未指定時のダミー関数
        """
        return {}

    def decition_name(self):
        """
        選択確定ボタンを押した時の処理
        """
        # コンボボックスの現在表示値を取得
        self.value.set(self.name_box.get())
        # 対応する表示情報の辞書を取得
        infos = self.command(self.value.get())
        self.show_information(infos)

    def show_information(self, infos):
        """
        情報表示のサブウィンドウを開く

        param
            infos: (dict)表示する情報の辞書(key: 項目名, val: 情報の内容)
        return
            None
        """
        # 情報表示用テーブル作成
        sub_win = tkinter.Toplevel()
        information = ttk.Treeview(
            master=sub_win, columns=("値"), show="tree",
            height=len(infos.items())
        )
        for key, val in infos.items():
            information.insert("", "end", text=key, values=(val))
        information.pack()
        # 閉じるボタン作成
        close_btn = tkinter.Button(
            master=sub_win, text="閉じる", command=sub_win.destroy
        )
        close_btn.pack(pady=10)


def main():
    # サンプルとするネコのインスタンス生成
    animals = AnimalInquirer()
    animals.increase_cat(
        "ジジ", date(2017, 5, 10), "オス", "",
        "ニシンパイ", "黒", "白", "短毛種"
    )
    animals.increase_cat(
        "タマ", date(2015, 11, 23), "オス", "",
        "サンマ", "白", "白", "短毛種"
    )
    animals.increase_cat(
        "ニャース", date(2017, 1, 31), "オス", "ポケモン",
        "がんもどき", "アイボリー", "白", "短毛種"
    )
    animals.increase_elephant(
        "ダンボ", date(2009, 3, 22), "オス", "ディズニー",
        "", "なし", "翼のように大きい", "短め"
    )

    # GUIの準備・設定
    root = tkinter.Tk()
    root.title("Cat's Infomations")
    app = OperationWindow(root, animals.get_names(), animals.to_dict)
    # 操作画面の表示
    app.mainloop()


if __name__ == "__main__":
    main()

今回は、ぞうの代表として"ダンボ"を登録してみた。

実行

>python .\src\main.py

image.png
ちゃんと選択肢にダンボが追加されている。

とりあえず、"ジジ"の情報を見てみる。
image.png
前と同じように見れている。

続いて"ダンボ"
image.png
ちゃんと後半3項目が"ぞう"専用のプロパティに変わっている。

まとめ

  • オブジェクト指向はすべてをオブジェクトとして考える
  • オブジェクトには、専用のプロパティ(≒変数)とメソッド(≒関数)を持たせられる
  • オブジェクトの持ち物は、クラスで定義する
  • 一部の既定の条件に対応する特殊メソッドが存在する
  • クラスはただの定義、プログラム内で使うにはインスタンスを作ってメモリ上に実体を作る
  • クラスは共通部分を継承することができる
  • 継承したメソッドはオーバーライドできる

最後に

サンプルとか考えながら書いてみたら結構なボリュームになってしまった。
書いておいてなんだが、オブジェクト指向の便利さとか難しさは一生懸命に文書呼んでもピンとこない。

根性論ぽくて嫌なのだが、実際に自分でクラス定義してみて、「こうやってクラス分けたらいいかな?」とか「この機能は○○クラスに持たせよう!」とかやってみて、「なんかクラス間がうまくつながらない・・・」とか「かなりすっきりまとまった!」とか経験してちょっとずつ実感できるものな気がする。

参考

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3