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

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

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

# 執筆時の環境
- OS:Windows10
- Python: 3.7.3

## 本記事のサンプルソース

本記事に登場するサンプルソースは、GitHubに置いてある。
実行まで試したい人は、[ここ](https://github.com/TAKE4548/what_object_oriented)からどうぞ。

# オブジェクト指向とは

定義的なものはこのような感じらしい。
出典：[Wikipedia](https://ja.wikipedia.org/wiki/%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E6%8C%87%E5%90%91#%E3%82%B3%E3%83%B3%E3%82%BB%E3%83%97%E3%83%88)
>オブジェクト指向（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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291298/8868a886-b93e-c422-0b40-43ea0a134a36.png)

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

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

## プロパティ
まず、一言にネコといっても、世界には色んな種類がいるし、さらに"毛色”や"目の色”、"性別"、"長毛か短毛か"など、ちょっと考えただけで沢山の項目が思い浮かぶ。
![ネコの要素.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291298/6131303b-561f-792e-3f81-e03dd50efceb.png)
このように、"そのオブジェクト"を示す特徴になる固有の情報を示す項目を**プロパティ(属性)**という。
上で出した例からすると、ネコのプロパティは…

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

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

例えば、猫カフェでタブレットを設置しておいて、**"ネコの名前をタップしたら、年齢、どんな種類か？、好物は何か？"などの情報を表示してくれるアプリを作成する**とした場合、上の例で定義したプロパティだけでは"名前"を選んでもどのネコの情報なのか検索しようとしてもできないし、"年齢"、"好物"という情報を取得しようとしてもプロパティが存在していないので、プロパティの定義が不十分であるということになる。
![ネコプロパティ例.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291298/18183d88-83ee-1787-fad8-6ac46900fce8.png)
なので、このアプリを実現しようと思うと、プロパティに次の情報を追加することになる。

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

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

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

```
root/
  └src/
    └cat.py
```

```python: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
```

```python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291298/04019de8-c051-fbc6-30f0-8aff8c5d8398.png)
このように、そのオブジェクトが持つ固有の振る舞い定義したものを**"メソッド"**という。

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

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

誕生日プロパティが追加できたら、メソッドの設計に入る。
今回の機能は、次のようなフローで実現できるはず。
![年齢取得フロー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291298/ef8d153d-4d7e-aac5-3ccc-43edf3748622.png)
実際に`Cat`クラスを改造してみよう。

```python: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`も少し修正して実行してみる。

```python: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月)。

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

```python:cat.py
def Cat:
    # ~省略
    def calculation_age(self):
        # 省略
```
の部分で**"誕生日"から"年齢"を設定する処理**を定義しているメソッドになっていて、

```python: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関数で表示してみると、

```python: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`を実行してみる。

```python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291298/82bce2f9-8ba1-e858-c0e1-6e0c204a11fb.png)
一番上はクラス名で、その下にプロパティ一覧が並び、さらに下にメソッド一覧が並ぶ。
今回は日本語で書いてみたが、実装とのリンク性を強めたければ、ソース上のプロパティ名やメソッド名をそのまま書くべきだし、逆にあまり詳しくない人に抽象的に説明する場合は、日本語に読み替えたりしたほうがわかりやすい。

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

```python: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つを選んで表示する"という機能なので、選択肢の提供と選択結果との照合をするために作成した。

```python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291298/85e6b5ab-acc1-3039-f90b-769b680002a1.png)

コンボボックスをクリックすると、ドロップダウンリストが出てくる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291298/d6ed9667-76f8-9bd4-7be0-77eb06068d10.png)
ここに出てくる選択肢は、

```python
app = OperationWindow(root, cats.get_names(), cats.to_dict)
```
の第2引数で渡したリストの中身が設定されている。

好きなネコを選んで、`select`ボタンを押すと別のウィンドウが開いて、次のような情報が表示される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291298/edf780f9-6761-27b7-6f04-5cff87a2e17d.png)
ここで表示する情報は、

```python:main.py
app = OperationWindow(root, cats.get_names(), cats.to_dict)
```
の第3引数で指定した関数の戻り値(`dict`である必要がある)になっていて、今回の`cats.to_dict`だと、

```python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291298/5e7ff08a-7c3d-7192-602f-171b21f50040.png)

そうすると今度は「`Cat`クラスをコピーして、それぞれの動物のクラスを作れば解決じゃん」という考えがよぎる。
実際、それぞれの動物別にクラスを作れば、プロパティは"その動物のものだけ"を定義することになるので、**考え方としては間違っていない。**
一方で、動物園で飼っている以上、"名前"、"誕生日"、"年齢"というプロパティは**どんな動物のクラスでも定義するものだ**というのが想像できるし、当然、年齢算出メソッドも動物によって処理が変化するものではないことがわかる。
![動物園 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291298/0777a9e6-3374-86b3-9da6-ab78b807b969.png)

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

この問題を解決するのが、**"継承"**という概念になる。
その名の通り、共通する部分のみを定義したクラス(**基底クラス**という)を作って、他のクラスに"継承"させる。
"継承"されて作られたクラス(**サブクラス**という)は、"基底クラス"のメソッドやプロパティも使える状態で、各々のクラス専用のプロパティやメソッドを追加定義することができる。
![2019-09-05_03-18_cat_class-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291298/74c66f78-476a-e510-672e-fd07b154a5d7.png)

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

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

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

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

```python: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`クラスを継承したことで、こんなにシンプルになった。

```python: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`クラスと同じ。

```python: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().`という記述があるが、これはサブクラスから基底クラスの要素にアクセスすることを表している。

```python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291298/05a7d287-4574-6008-45dc-1d26b8a0c97e.png)

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

```python: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()`メソッドがあるようだ。

```python: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`と差異はない。

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

```python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291298/7f00e4f4-d553-c77c-c661-e52b814dc4cc.png)
ちゃんと選択肢にダンボが追加されている。

とりあえず、"ジジ"の情報を見てみる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291298/44f77216-009e-338a-44ff-fc0135715ff6.png)
前と同じように見れている。

続いて"ダンボ"
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291298/7cf2993a-3fb3-e657-c32d-dc48e7d786f1.png)
ちゃんと後半3項目が"ぞう"専用のプロパティに変わっている。

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

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

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

# 参考
- [オブジェクト指向-Wikipedia](https://ja.wikipedia.org/wiki/%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E6%8C%87%E5%90%91#%E3%82%B3%E3%83%B3%E3%82%BB%E3%83%97%E3%83%88)
- [【Python入門】クラス利用時の特殊メソッド一覧（サンプルコード付き）](https://blog.codecamp.jp/python-class-code)