Pythonで〇×ゲームのAIを一から作成する　その93　継承を利用したGUIの定義

# 目次と前回の記事

https://qiita.com/ysgeso/items/2381dd4e3283cbed49a0

https://qiita.com/ysgeso/items/0bad81b51c0ccbdbc3c7

## これまでに作成したモジュール

以下のリンクから、これまでに作成したモジュールを見ることができます。

https://github.com/ysgeso/marubatsu/blob/master/093/marubatsu.py

https://github.com/ysgeso/marubatsu/blob/master/093/ai.py

https://github.com/ysgeso/marubatsu/blob/master/093/util.py

https://github.com/ysgeso/marubatsu/blob/master/093/tree.py

## ルールベースの AI の一覧

ルールベースの AI の一覧については、下記の記事を参照して下さい。

https://qiita.com/ysgeso/items/10d1d01192c014173e4b

# ゲーム木を視覚化する GUI の作成

前回の記事で、ゲーム木の視覚化を行うために必要となる、特定のノードを指定して、下記のような部分木を描画するように `draw_tree` メソッドを修正しました。

* ルートノードからそのノードの局面までの推移を表す、祖先ノードを描画する
* 親ノードの子ノードを描画する
* 特定の深さまで子孫ノードを描画する

今回の記事では、上記の処理を使って __ゲーム木の視覚化を行う GUI__ の作成を開始することにします。そのためには、どのような GUI でゲーム木の視覚化を行うかを決める必要があります。その方法について少し考えてみて下さい。

## ゲーム木を視覚化する GUI の仕様

`draw_tree` メソッドは、__特定のノードを指定__ し、その __周辺のノードの部分木を描画__ する処理を行います。従って、`draw_tree` を使ってゲーム木を視覚化する GUI では、__その特定のノードを変更__ することで、__描画する部分木を移動__ できるようにする必要があります。

具体的には、`draw_tree` を描画する際に指定した下図の __赤丸のノード__ を、__GUI の操作__ によって __別のノードに移動して `draw_tree` で描画__ することで、描画するゲーム木の __部分木の範囲を自由に変更できる__ ようにします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/4f335afc-b034-069b-a995-83158e558021.png" width="350">


その際に、__遠くのノードに移動__ すると、部分木の __描画が大きく変わってしまう__ ため、見た目が __直観的にわかりづらくなる__ ので、移動は __近いノード__ に対して行ったほうが良いでしょう。そこで、本記事では下記の 4 種類の移動を GUI で行えるようにすることにします。

* 親ノードへ移動
* 一つ前の兄弟ノードへ移動
* 一つ後の兄弟ノードへ移動
* 子孫ノードの中の先頭のノードへ移動

また、それぞれの移動は、〇×ゲームの GUI で手数を移動する GUI と同様に、__ボタンとキーによって行う__ ことにします。ボタンに表示する文字列とキーは、`draw_tree` が下記のように部分木の描画を行うので、下記の表のように設定することにします。

* 左から右に、深さ 0 のルートノードから浅い順にノードを描画する
* 兄弟ノードは、上から下に順番に描画する

|  | ボタンの表示と対応するカーソルキー |
|:--|:-:|
| __親ノードへ移動__  | ←  |
| __一つ前の兄弟ノードへ移動__  | ↑  |
| __一つ後の兄弟ノードへ移動__  | ↓  |
| __子孫ノードの中の先頭のノードへ移動__  | →  |

## `Mbtree_GUI` の定義

ゲーム木を視覚化する GUI は、〇×ゲームを遊ぶための GUI を実装した際に定義した __Marubatsu_GUI クラスと同様の方法__ で実装することができます。そこで、ゲーム木を GUI で視覚化するための __Mbtree_GUI というクラスを定義して実装する__ ことにします。

下記は、`Marubatsu_GUI` クラスの `__init__` メソッドの定義です。

```python
 1  class Marubatsu_GUI:
 2      def __init__(self, mb, ai_dict=None, seed=None, size=3):
 3          # JupyterLab からファイルダイアログを開く際に必要な前処理
 4          root = Tk()
 5          root.withdraw()
 6          root.call('wm', 'attributes', '.', '-topmost', True)  
 7
 8          # save フォルダが存在しない場合は作成する
 9          if not os.path.exists("save"):
10               os.mkdir("save")        
11        
12          # ai_dict が None の場合は、空の list で置き換える
13          if ai_dict is None:
14              ai_dict = {}
15
16          self.mb = mb
17          self.ai_dict = ai_dict
18          self.seed = seed
19          self.size = size
20
21          # ai_dict が None の場合は、空の list で置き換える
22          if ai_dict is None:
23              self.ai_dict = {}
24
25          # %matplotlib widget のマジックコマンドを実行する
26          get_ipython().run_line_magic('matplotlib', 'widget')
27        
28          self.disable_shortcutkeys()
29          self.create_widgets()
30          self.create_event_handler()
31          self.display_widgets() 
他のメソッドの定義は省略
```

上記のプログラムのうち `Mbtree_GUI` で __必要な処理__ と __必要でない処理__ は以下の通りです。

* __必要__ な処理
    * __26 行目__：GUI で maplotlib の Figure を表示する場合は `%matplotlib widget` のマジックコマンドを実行する必要がある
    * __28 ~ 31 行目__：デフォルトのショートカットキーの禁止、ウィジェットの作成、イベントハンドラの定義、ウィジェットの表示に関する処理は、`Mbtree_GUI` でも必要

* __必要でない__ 処理
    * __4 ~ 10 行目__：`Mbtree_GUI` はファイルを扱わない
    * __13 ~ 23 行目__：〇×ゲームの GUI では必要だが、`Mbtree_GUI` では不必要

上記から、`Mbtree_GUI` の `__init__` メソッドを下記のように定義する方法が考えられます。なお、他の処理に関しては、今後の記事で実装を行います。

```python
class Mbtree_GUI:
    def __init__(self):       
        # %matplotlib widget のマジックコマンドを実行する
        get_ipython().run_line_magic('matplotlib', 'widget')
        
        self.disable_shortcutkeys()
        self.create_widgets()
        self.create_event_handler()
        self.display_widgets() 
```

しかし、これまで何度も説明したように、上記の __全く同じ処理__ を Marubatsu_GUI と Mbtree_GUI の __複数の個所に記述しないほうが良い__ でしょう。また、上記の処理は、__ウィジェットを利用して GUI を作成する際に必須__ となる処理なので、__別の GUI を作成__ しようと思ってクラスを定義する際も、上記と __同じ処理を記述する必要__ があります。そのため、上記の処理は何らかの方法で 1 箇所にまとめて記述したほうが良いでしょう。

__複数のクラス__ で __共通する処理をまとめる方法__ に、__委譲__ と __継承__ があります。今回の記事では、委譲と継承について説明し、継承を使って GUI のクラスを定義する事にします。

# 委譲（composition）

__委譲__ とは、__クラスに新たな機能を追加__ する際に、その __新たな機能の処理を行う別のクラスを定義__ し、そのクラスの __インスタンスを使って機能の追加を行う__ というものです。あるクラスで行う処理を、他のクラスに委譲（委任）することからそのように呼ばれます。

:::note
composition を集約、合成と呼び、委譲をデリゲーション（delegation）と呼ぶ場合もあり、それぞれの意味も本記事で説明したものと若干異なる場合があるようですが、本記事では委譲という用語だけを、上記の意味で使うことにします。
:::

実は、委譲は [以前の記事](https://qiita.com/ysgeso/items/05ac74365dad096dd28f#%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AB%E3%82%88%E3%82%8B-gui-%E3%81%AE%E6%A9%9F%E8%83%BD%E3%81%AE%E5%88%86%E9%9B%A2) で Marubatsu クラスの `play` メソッドから __GUI の機能を分離__ する際に、下記のような方法で既に行っています。

* 〇×ゲームの __GUI の機能だけを扱う__、Marubatsu_GUI クラスを定義する
* Marubatsu クラスの `play` メソッドの中で `mb_gui = Marubatsu_GUI(略)` を記述して Marubatsu_GUI クラスの __インスタンスを作成__ し、その __メソッドを呼び出す__ ことで 〇×ゲームの __GUI の処理を行う__

一般的に、クラスの __機能を増やすと__、そのクラスは __複雑になります__。その結果、バグが発生しやすくなったり、クラスの修正を行うことが困難になります。委譲を行うことで、クラスの機能を拡張する際に、新しい機能を別のクラスで定義することになるので、元のクラスの複雑化を緩和することができるようになります。

委譲はイメージ的には、機械に機能を追加する際に、その機械の設計図に新しい機能を書き加えるのではなく、その機能を持つ __部品の設計図を作成__ し、その設計図から作られた __部品を機械に組み込む__ ということに相当するでしょう。

また、委譲を行うことで、__新しい機能の処理__ が、__新しいクラスの定義の中でまとまって記述される__ ので、プログラムがわかりやすくなるという利点もあります。例えば、Marubatsu クラスでは、最初は `play` メソッド内に GUI の機能を記述していましたが、その場合は、Marubatsu クラスの定義の中のどの部分に `GUI` の処理が記述されているかを、プログラムの作者以外の人[^1]が見つけることは簡単ではないでしょう。一方、Marubatsu_GUI クラスを定義する事で、GUI の処理がどこに記述されているかがわかりやすくなります。

ただし、クラスに新しい機能を追加する際に、__常に委譲を行えば良いというわけではありません__。委譲を行いすぎると今度は小さなクラスが増えすぎてプログラムがかえってわかりづらくなる場合があるので __バランスが重要__ ですが、そのバランス感覚は様々なプログラムを実際に実装することでしか身に付けることはできません。委譲は今後の記事でも利用することになるので、その際にまた説明を行う予定です。

# 継承（inheritance）

クラスの __継承__ は、あるクラスが、別のクラスの属性やメソッドなどを利用できるようにするための仕組みです。__継承元__ のクラスを __基底__（base）クラス、__継承先__ のクラスを __派生__ （derived）クラスと呼びます。

なお、基底クラスと派生クラスには、下記のような呼び方もありますが、本記事では Python のドキュメントに倣って、基底クラスと派生クラスという用語を使うことにします。

|  | 別の呼び方 |
|:--|:--|
| __基底クラス__  | 親（parent）クラス、スーパー（super）クラス  |
| __派生クラス__  | 子（child）クラス、サブ（sub）クラス  |

クラスの継承の詳細については、下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/tutorial/classes.html#inheritance
## 継承の説明に関する補足

継承の説明を行う前に、いくつかの補足説明を行います。

__プログラム言語によって__、__クラスの継承__ が具体的にどのように行われるかが __異なる場合があります__。また、__クラスの継承の使い方__ に関しては、様々な意見があり、__統一されているとは言えない状況にある__ と思います。そのため、継承に関する説明は、書籍や記事によってかなり異なる場合が多いようです。

正直に言いますが、筆者も継承を完璧に使いこなすことができる自信はありません。従って、この記事での継承の説明は、__Python での継承の使い方__ に関して、__筆者なりの理解を説明したもの__ だと考えて下さい。

## 継承の利用目的

継承の利用目的の一つに、複数のクラスで共通する処理を基底クラスに定義することで、__プログラムの重複を解消する__ というものがあり、昔はこの目的で継承が使われていたことが多かったようですが、__現在では__ そのような使い方は弊害が多いため一般的には __推奨されなくなっています__。そのような場合は、先程説明した __委譲を使うことが推奨__ されています。

継承によって __弊害が起きる原因__ の一つは、継承は委譲と異なり、派生クラスが __基底クラスの性質をすべて受け継ぐ__ ことになるため、__派生クラス__ は基底クラスを __複雑にしたものになる__ からです。そのため、共通の性質を持つという理由だけで継承を乱用してしまうと、クラスの構成が非常に複雑になってしまい、保守性の低いプログラムになってしまいます。

もう一つの利用目的は、__共通の性質を持つもの__ を __別々のクラスで定義__ する際に、それらの __共通の性質__ を表すクラスを __基底クラスとして定義__ し、その基底クラスから __継承を行うことで個々のクラスを定義する__ というものです。そのようにすることで、同じ基底クラスから __派生した様々なクラス__ を、__共通した方法で扱うことができる__ ようになります。

例えば、様々な図形を扱うことができるお絵かきソフトを作成する場合のことを考えてみて下さい。図形には共通する性質として、枠の太さ、枠の色、塗りつぶしの色などがあります。また、お絵かきソフトは図形を画面に描画する必要があります。そこで、下記のような Figure という名前のクラスを基底クラスとして定義します。

* 枠の太さ、枠の色、塗りつぶしの色などを表す属性を持つ
* 図形を描画するメソッドを持つ

そして、三角形、四角形などの、そのお絵かきソフトで扱うことができる様々な図形を扱うクラスを、Figure を基底クラスとする派生クラスとして定義することで、すべての図形が共通する属性と、図形を描画する共通のメソッドを利用できるようになるという利点が得られます。なお、この説明では意味がわからない人が多いと思いますが、言葉の説明だけでこの利点を理解するのはほぼ不可能だと思います。現時点ではそういうものだという、雰囲気だけを理解して、後で意味がわかるようになってから復習すればよいでしょう。

:::note warn
クラスの継承は、オブジェクト指向プログラミングを特徴づけるものの一つで、うまく利用すると非常に便利ですが、乱用するとプログラムがかえって複雑になったり、プログラムの修正が思わぬ悪影響を与えてしまうことがあります。そのため、クラスの継承はなるべく行うべきではないという人もいるようです。

例えば、上記で説明した、現在推奨されていない目的での継承は、継承の使い方を学べば誰でも簡単に記述できてしまうため、初心者が乱用しがちです。その結果、良くないプログラムを記述する悪い癖がついてしまうため、継承を嫌う人がいます。

ただし、継承をうまく使えば便利であることは間違いはありませんし、モジュールの中には、継承の利用を前提とするようなものもあるので、継承の使い方を学んでおく必要はあると思います。

そこで、本記事では継承を行って GUI の処理を行うクラスを定義する事にします。

初心者の方は、どのような場合にクラスの継承を利用すべきであるかの判断は難しいと思いますが、何でもかんでもクラスの継承を使えば良いというわけではない事は覚えておいてください。例えば、ちょっとした共通点だけを見つけた時に、クラスの継承の機能を行うのは避けたほうが良いでしょう。

完璧な指針ではありませんが、どのような場合に継承を行うべきかの指針はあるので、それについてはこの後で説明します。
:::



## クラスの継承と is-a、has-a 関係

__クラスの継承を行うべきか__ どうかを見分ける __指針__ の一つに __is-a__ と __has-a__ 関係があります。

### is-a 関係

__is-a__ 関係は、A is a B[^2]、すなわち __A は B（の一種）である__ という関係で、クラス A、B に __この関係がない場合__ はクラスの __継承を行うべきではない__ という指針があります。

例えば、__三角形__ と __図形__ は、__三角形は図形（の一種）である__ という関係があるので、三角形を表す Triangle と、図形を表す Figure というクラスを定義した場合は、Figure を基底クラス、Triangle を派生クラスとしてクラスの継承を行っても良い[^3]ということになります。

クラスが A is a B という is-a の関係であれば、共通する性質を基底クラスにまとめることで、継承によって共通の性質を持つ派生クラスを多数記述できるという利点が得られます。

なお、A は B の中の一つであることから、A を B の __特化__ と呼びます。また、B は A を一般化したものであると考えることができるので B を A の __汎化__ と呼びます。

### has-a 関係

is-a 関係と似た関係に __has-a__ 関係というものがあります。__A has a B__ という関係は、「A は B（の機能）を持っている」、「A には B がある」という __機能や特性__ の関係です。

例えば「車にはドアがある」、「車にはタイヤがある」、「車は運転ができる」などは has-a の関係で、このような関係の事を __包含関係__ と呼びます。

「ドアは車である」という文は間違っているので、車とドアには has-a の関係はありますが、is-a の関係はありません。このような場合は、車にはドアという共通の要素があるからと言って、ドアを表す Door を基底クラス、車を表す Car を派生クラスとしてクラスの継承を __行うべきではなく__、先程説明した __委譲を使うべき__ です。

残念ながら、is-a や has-a の関係を持っているかどうかを判別しづらい場合があったり、__一見すると is-a の関係を持っている__ が __継承を行うべきではない場合がある__ のでややこしいのですが、__原則__ として、__is-a の関係を持たないクラス__ は、__一般的にはクラスの継承を行うべきではない__ と考えると良いでしょう。

## 委譲と継承の違い

クラスの継承と委譲の主な違いは以下の通りです。

* 継承は __基底クラスを拡張__ するという形で新たな機能を付け加えた派生クラスを作成する。派生クラスは __基底クラスの全ての属性とメソッドを利用できる__
is-a の関係にない場合は継承を利用すべきではない
* 委譲は付け加えたい機能を処理する __別のクラスを定義__ し、そのクラスの __インスタンスを作成して利用する__ という形でクラスに新しい機能を付け加える。定義した別のクラスは独立しているので、__元のクラスの機能__ を直接 __利用することはできない__
has-a の関係にある場合は委譲を利用すべきである

__Marubatsu_GUI__ は、〇×ゲームの GUI の機能の処理を行うクラスで、__Marubatsu__ は 〇×ゲームの処理を行うクラスです。「〇×ゲームの GUI は、〇×ゲームである」も、「〇×ゲームは、〇×ゲームの GUI である」も __成り立たない__ ので、この __2 つのクラスの関係は is-a でありません__。一方、〇×ゲームの GUI は、〇×ゲームを遊ぶ際の __機能の一つ__ なので、__has-a の関係にあります__。このような場合は、継承ではなく、__委譲を使うべき__ で、実際に本記事でも委譲を使って、Marubatsu クラスに GUI の機能を追加しました。

## クラスの継承の記述方法

クラスの継承の概要を説明したので、次は具体的な記述方法について説明します。

クラスの継承を行った派生クラスは、下記のプログラムのように定義します。

```python
class 派生クラス名(基底クラス名):
    # 以下、派生クラスの定義を記述する
```

上記のように記述することで、継承クラスは、基底クラスのすべての属性やメソッドを利用することができるようになります。

:::note
Python では、複数のクラスを同時に継承する派生クラスを定義することができ、そのような場合の事を __多重継承__ と呼びます。ただし、多重継承は、基底クラスに同じ属性やメソッドが定義されていた場合、どちらを派生クラスで利用できるかがわかりづらくなるという大きな問題があるので、あまり好まれてはいないようです。

本記事でも、どうしても必要になる場合を除いて多重継承は利用しません。
:::

これだけではピンとこないと思いますので、具体例を示します。

まず、継承を行うために必要となる、基底クラスとなるクラス A を定義します。クラス A は、`__init__` メソッドで `x`、`y` という属性に 1 と 2 を代入し、属性 `x` を表示する `printx` というメソッドを持ちます。従って、下記のプログラムの 9、10 行目のように クラス A のインスタンスを作成し、`printx` を呼び出すと、実行結果のように 1 が表示されます。

```python
 1  class A:
 2      def __init__(self):
 3          self.x = 1
 4          self.y = 2
 5          
 6      def printx(self):
 7          print(self.x)
 8
 9  a = A()
10  a.printx()
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
class A:
    def __init__(self):
        self.x = 1
        self.y = 2
        
    def printx(self):
        print(self.x)

a = A()
a.printx()
```
</details>

実行結果

```
1
```

下記のプログラムは、__A を基底クラス__ とする __B という派生クラスを定義__ するプログラムです。__クラス B は クラス A を継承__ しているので、__クラス A の属性とメソッドをすべて利用することができます__。そのため、5 、6 行目のように、クラス B のインスタンスを作成し、`printx` を呼び出すと、実行結果のように 1 が表示されます。また、7 行目のように 属性 `x` を直接 `print` で表示することもできます。

また、クラス B には クラス A の属性 `y` を表示する __`printy`__ メソッドが定義されているので、8 行目のように `printy` を呼び出すと、実行結果のように 2 が表示されます。

```python
1  class B(A):
2      def printy(self):
3          print(self.y)
4     
5  b = B()
6  b.printx()
7  print(b.x)
8  b.printy()
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
class B(A):
    def printy(self):
        print(self.y)
        
b = B()
b.printx()
print(b.x)
b.printy()
```
</details>

実行結果

```
1
1
2
```

なお、__継承元__ となった __クラス A のインスタンス__ は、__派生先の__ クラス B で定義された __メソッドを利用することはできません__。下記のプログラムのように クラス B で定義された `printy` を `a` に対して呼び出そうとすると実行結果のようなエラーが発生します。

```python
a.printy()
```

実行結果

```
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[3], line 1
----> 1 a.printy()

AttributeError: 'A' object has no attribute 'printy'
```

:::note warn
クラスの継承によって派生クラスのインスタンスは、基底クラスの属性とメソッドを利用できるようになるが、基底クラスのインスタンスが派生クラスのインスタンスを利用することはできない。

つまり、クラスの継承は、__基底クラスから派生クラスへの一方通行__ である。
:::

## メソッドのオーバーライド（override）

__派生クラス__ には、__基底クラス__ で定義されたメソッドと __同じ名前のメソッドを定義__ することができます。その場合に派生クラスのインスタンスからそのメソッドを呼び出すと、__派生クラスで定義されたメソッドが呼び出される__ ようになります。

このように、派生クラスで基底クラスと __同じ名前のメソッドを定義__ することを、メソッドの __オーバーライド__（override）、または上書きと呼びます。

例えば、下記のプログラムのように、基底クラスである A が持つ `printx` と同じ名前のメソッドをクラス B に定義することができます。なお、クラス A が持つ `printx` 区別できるように 2 行目でメッセージを表示するようにしてみました。

```python
def printx(self):
    print("printx of class B")
    print(self.x)

B.printx = printx
```

上記のプログラムを実行した後で、下記のプログラムのように、クラス B のインスタンスから `printx` を呼び出すと、実行結果にクラス B の `printx` で表示するメッセージが表示されることから、__クラス B の `printx` が呼び出されている__ ことがわかります。

```python
b.printx()
```

実行結果

```
printx of class B
1
```

また、下記のプログラムのように、クラス A のインスタンスから `printx` を呼び出すと、実行結果から、クラス A の `printx` が呼び出されてることがわかります。

```python
a.printx()
```

実行結果

```
1
```

### オーバーライドの目的と基底クラスのメソッドの呼び出し

メソッドをオーバーライドすることの __目的__ には、以下のようなものがあります。

* そのメソッドで、基底クラスのメソッドと __全く異なる処理を行いたい__ 場合
* そのメソッドで、基底クラスのメソッドで __行った処理に加えて、別の処理を付け加える__ という、__メソッドの機能の拡張__ を行いたい場合

前者の場合は単にオーバーライドすればよいだけですが、後者の場合は、__基底クラスのメソッドを呼び出す__ 必要があります。そのような場合は、下記のいずれかの方法で基底クラスのメソッドを呼び出すことができるようになります。

* __`基底クラスの名前.メソッド名(self, 実引数)`__ を記述する
* `super` という組み込み関数を使って、__`super().メソッド名(実引数)`__ を記述する

「基底クラスの名前を記述しなくても良い」、「実引数に `self` を記述しなくても良い」などの理由で、__`super` のほうが良く使われている__ と思います。

組み込み関数 `super` の詳細については、下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/library/functions.html#super

具体例を挙げます。

先程 B というクラスで定義した `printx` は、メッセージを表示してから属性 `x` の内容を表示しましたが、__属性 `x` の内容を表示する処理__ は、基底クラスである __A の `printx` で実装済__ です。従って、クラス B の `printx` には、下記の処理を記述することで、__基底クラス A の `printx` の機能を利用__ しながら __先程と同じ処理を行う__ ことができます。

* メッセージを表示する
* `super().printx()` によって、基底クラスの `printx` を呼び出して属性 `x` の値を表示する

:::note warn
2024/09/05 追記

__`super`__ を __クラスの定義の外で記述されたメソッドで利用できる__ ことが判明しました。詳細は [こちらの記事](https://qiita.com/ysgeso/items/903dc60e865ff56191fc#super-%E3%81%AE%E4%BF%AE%E6%AD%A3%E3%81%AB%E9%96%A2%E3%81%99%E3%82%8B%E8%AA%AC%E6%98%8E) を参照して下さい。なお、`super` に関する内容を記事をさかのぼってすべて修正するのは大変なので、そのまま残しておくことにします。
:::

:::note alert
ただし、__`super`__ は、__クラスの定義の中で記述されたメソッドの中でしか利用できない__ ので、下記のような、通常の関数としてメソッドを定義し、その関数をクラスの属性に代入するという方法は利用できない点に注意が必要です。

```python
def printx(self):
    print("printx of class B")
    super().printx()

B.printx = printx
```

上記のプログラムを VSCode で記述すると、下図のように `super` の部分にエラーを表す赤い波線が表示されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/c3569bf9-40f1-f8b1-9803-100927b149a2.png" width="200">

上記のプログラムを実行してもエラーは発生しませんが、下記のプログラムを実行して `printx` を呼び出すと、`super` の処理で実行結果のようなエラーが発生します。

```python
b.printx()
```

実行結果

```
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Cell In[8], line 1
----> 1 b.printx()

Cell In[7], line 3
      1 def printx(self):
      2     print("printx of class B")
----> 3     super().printx()

RuntimeError: super(): __class__ cell not found
```

`super` を利用するためには、`super` が記述されたメソッドがどのクラスで定義されているかの情報が必要になりますが、通常の関数として `printx` を定義した場合は、その情報を得ることができません。__class cell not found__ というエラーメッセージは、上記の `printx` がどのクラスで定義されたメソッドであるかの情報を得ることができなかったという意味を表します。
:::

従って、正しくは下記のプログラムのように クラスの定義の中で `printx` を記述します。

```python
class B(A):
    def printx(self):
        print("printx of class B")
        super().printx()
```

__クラス B を定義し直した__ ので、下記のプログラムの 1 行目のように、__クラス B のインスタンスを作成し直す__ 必要があります。2 行目で `printx` を呼び出すと、実行結果のようにメッセージを表示した後に、基底クラス A の `printx` が呼び出されて 1 が表示されます。

```python
b = B()
b.printx()
```

実行結果

```
printx of class B
1
```

:::note
`super` は、`super` が記述されたメソッドが定義されたクラスの __基底クラスを探す__ という処理を行いますが、`super` を使わずに、下記のプログラムのように、基底クラスの名前と実引数 `self` を記述した場合は、基底クラスを探す必要がないので、通常の関数として `printx` を定義しても、実行結果のようにエラーは発生しません。

```python
def printx(self):
    print("printx of class B")
    A.printx(self)

B.printx = printx

b.printx()
```

実行結果

```
printx of class B
1
```
:::

## 仮想関数

__派生クラス__ で __オーバーライドできるメソッド__ の事を、__仮想関数__（virtual function）と呼びます。プログラム言語によっては、基底クラスで定義したメソッドが、__派生クラスでオーバーライドできるか__ どうかを __設定できる__ 場合があります。そのようなプログラム言語では、仮想関数でないメソッドを、派生クラスでオーバーライドしようとするとエラーが発生します。ただし、__Python__ ではすべてのメソッドが派生クラスで必ずオーバーライドできるので、__すべてのメソッドは仮想関数である__ と言えます。

### 抽象メソッドと抽象クラス

__is-a の関係__ にあるクラスを継承する場合は、__同じ基底クラスから派生した様々なクラス__ を、__共通した方法で扱う__ ことができるようにすると __便利__ です。

具体的には、__基底クラス__ で定義した属性やメソッドと __同じ名前の属性やメソッド__ を __派生クラスで利用する__ ようにします。そのようにすることで、同じ基底クラスから派生したクラスを、__共通した方法で扱う__ ことができるようになるという __大きな利点__ が得られます。

具体例を挙げます。先ほどの例で挙げた、__様々な図形を扱う__ ことができるお絵かきソフトを作成する場合は、__図形を表す Figure__ という __基底クラス__ に、__図形を描画__ する __`draw`__ という __仮想関数を定義__ します。ただし、図形の種類によって描画の方法が完全に異なるので、基底クラスの `draw` に図形を描画する処理を記述することは不可能です。そこで、下記のプログラムのように、__Figure の `draw`__ は __何の処理も行わないメソッドとして定義__ します。

```python
class Figure:
    def draw(self):
        pass
```

上記の `draw` のように、__派生クラスでオーバーライドすることを前提として定義__ された、処理が定義されていない仮想関数を、__抽象メソッド__、純粋（完全）仮想関数などと呼びます。

また、__抽象メソッドが定義されたクラス__ の事を、__抽象クラス__、抽象クラスから派生した、__具体的な処理を実装__ するクラスの事を __具象クラス__ と呼びます。

:::note
上記の `draw` メソッドは、__何もしないという処理が定義されている__ ので、実は純粋な __抽象メソッドではありません__。また、__Python__ というプログラム言語は、本当の意味で処理が定義されていない、厳密な __抽象メソッドを定義する機能を持ちません__。

同様に、Python は __抽象クラスを定義する機能を持ちません__。これらのことは、この後で説明します。
:::

実際の描画を行う処理は、例えば __Figure を継承__ した __三角形を表す Triangle__ というクラスの中に、下記のプログラムのように __`draw` をオーバーライドすることで定義__ します。

```python
class Triangle(Figure):
    def draw(self):
        # 三角形を描画する処理をここに記述する
```

このように、__基底クラスの中__ で、__派生クラスで利用する__ メソッドとなる __抽象メソッドを定義__ し、__派生クラスで具体的な処理を行うメソッドを定義__ して __オーバーライドする__ という方法でプログラムを記述することで、__基底クラス__ によって、__派生クラスに共通する操作環境__（インターフェース）を __決める__ ことができるようになります。

:::note
抽象クラスによって、具象クラスの操作環境が決められるので、そのようなクラスの事を __インターフェース__ と呼びます。
:::

### 抽象メソッドの機能を持つプログラム言語での抽象メソッドの性質

抽象メソッドを定義する機能を持つプログラム言語の多くでは、抽象メソッドは __具体的な処理が定義されていないメソッド__ なので、抽象メソッドを __呼び出して実行することはできません__。そのため、そのようなプログラム言語では、抽象メソッドが含まれる、__抽象クラスからインスタンスを作成することができない__ ようになっています。

また、そのことは __抽象クラスから派生したクラスでも同様__ で、__すべての基底クラスの抽象メソッド__ をオーバーライドして __具体的な処理を定義する__ 事ではじめてインスタンスを作成できるようになっています。

このように、__派生クラスで抽象メソッドのオーバーライドを強制__ することで、派生クラスでの __共通のインターフェースを保証__ することができます。

### Python でのメソッドの性質

しかし、先程ノートで説明したように、__Python__ には、__抽象メソッド__ や __抽象クラス__ を __定義する機能を持ちません__。下記のプログラムの、一見すると抽象メソッドのように見える `draw` というメソッドは、__何も行わないという処理が定義されている__ ため __抽象メソッドではありません__。従って、Figure は抽象クラスではないので 5 行目のようにインスタンスを作成することができ、6 行目のように `draw` を呼び出して実行しても、実行結果のように __エラーは発生しません__。

```python
1  class Figure:
2      def draw(self):
3          pass
4
5  f = Figure()
6  f.draw()
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
class Figure:
    def draw(self):
        pass

f = Figure()
f.draw()
```
</details>

実行結果

```
```

また、Python には、抽象メソッドの仕組みがないので、基底クラスで定義されたメソッドを、派生クラスで __オーバーライドしなくてもエラーが発生しません__。そのため、例えば、下記のプログラムのように、Figure を継承した Triangle というクラスに、__`draw` とは別の名前__ で、図形を描画するメソッドを定義してもエラーは発生しません。

また、下記のような、基底クラスの __メソッドと異なる名前__ でそのメソッドが行う処理を定義すると、基底クラスから派生したクラスに __共通する操作環境が得られなくなってしまいます__。例えば、同じ Figure クラスから派生したクラスの図形を描画するメソッドが、クラスによって `draw_triangle` だったり、`draw_circle` だったりすると非常に不便です。

```python
class Triangle(Figure):
    def draw_triangle(self):
        # 三角形を描画する処理をここに記述する
```

### Python での抽象メソッド、抽象クラスの機能の利用方法

このままでは不便なので、Python には __abc__ という __抽象メソッドと抽象クラスとほぼ同様の機能を利用できるようにする__ ための組み込みモジュールが用意されています。この機能を利用することで、他のプログラム言語の抽象メソッドと同様に、派生クラスでオーバーライドしないとエラーが発生するようなメソッドを定義することができるようになります。

なお、「抽象メソッドと同様の」という表現は冗長なので、以後はその方法で定義されたメソッドとクラスを、抽象メソッド、抽象クラスのように表記します。

:::note
abc は抽象基底クラスを表す abstract base class の略です。
:::

具体的には、以下のような方法で抽象メソッドと抽象クラスを定義します。

* __abc__ というモジュールから __`ABCMeta`__ と __`abstractmethod`__ をインポートする
* __抽象クラスとして定義__ するクラスを定義する際に、__`(metaclass=ABCMeta)`__ を記述する
* __抽象メソッドとして定義__ するメソッドの __定義の直前に、`@abstractmethod`__ という __デコレータを記述__ する。実際のメソッドの定義は何でもよいので記述する必要がある

下記は、具体的な記述例です。

```python
from abc import ABCMeta, abstractmethod

class Figure(metaclass=ABCMeta):
    @abstractmethod
    def draw(self):
        pass
```

なお、__`Figure(metaclass=ABCMeta)`__ の部分は、Python のメタクラスという機能を使った記述ですが、抽象メソッドの機能を利用する際に、メタクラスの意味を正しく理解する必要はないでしょう。正直に言うと、筆者はメタクラスを理解していません。興味がある方は下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/reference/datamodel.html#metaclasses

上記の定義後に、下記のプログラムのように、Figure を基底クラスとする Triangle クラスを、`draw` メソッドを定義せずに記述し、Triangle クラスのインスタンスを作成すると、実行結果のようなエラーが発生します。

```python
class Triangle(Figure):
    pass

t = Triangle()
```

実行結果

```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[14], line 4
      1 class Triangle(Figure):
      2     pass
----> 4 t = Triangle()

TypeError: Can't instantiate abstract class Triangle with abstract method draw
```

上記のエラーメッセージは、抽象メソッド（abstract method）である `draw` がオーバーライドされていないため、Triangle クラスが抽象クラス（abstract class）になっており、クラスのインスタンス化が行えなかったことを意味します。

下記のプログラムのように、Triangle クラスに __`draw` メソッドを定義してオーバーライド__ すれば、上記の __エラーは発生しなくなります__。なお、どのような形であれ、オーバライドを行えば良いので、下記のプログラムのように、何の処理も行わない `draw` メソッドの定義でも問題はありません。また、Triangle クラスには `__init__` メソッドを定義していないので、インスタンス化を行った際には何の処理も行われず、実行結果には何も表示されません。

```python
class Triangle(Figure):
    def draw(self):
        pass

t = Triangle()
```

実行結果

```
```

abc モジュールの詳細については、下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/library/abc.html#module-abc

# クラスの継承を利用した `Marubatsu_GUI` クラスの定義

GUI の処理を、クラスの継承を使って記述するための知識を説明したので、今回の記事では __Marbatsu_GUI__ クラスを、__クラスの継承を使って定義し直す__ ことにします。なお、大変だと思う人がいるかもしれませんが、実際に行う作業はあまり多くはありません。

まず、__基底クラス__ として、GUI に関する __共通の処理__ と、派生クラスで必要となる __抽象メソッドなどの定義__ を行う、__GUI__ という名前の __クラスを定義__ する事にします。Marubatsu_GUI が行う処理は、GUI の一種であるため、Marubatsu_GUI クラスと GUI クラスは __is-a の関係にある__ ため、クラスの継承を行っても構わないでしょう。

GUI クラスには、__すべての GUI で共通する処理__ を記述するので、Marubatsu_GUI の定義から、__すべての GUI で共通する処理を抜き出して記述する__ ことで定義することができます。

## GUI クラスの定義

下記は、GUI クラスの定義で、以下の方針でそれぞれのメソッドを定義しました。

* `class GUI(metaclass=ABCMeta)` を記述することで、抽象メソッドを持つ、抽象クラスとして GUI クラスを定義できるようにする
* `__init__` メソッドには、__GUI で必要な処理だけを抜き出して記述__ する
* `create_widgets`、`create_event_handler`、`display_widgets`、`update_gui` 、`update_widgets_status` は、いずれも __GUI の処理で共通する必須となる処理__ だが、GUI の種類によって行う処理が異なるので、何も処理を行わない __抽象メソッド__ として、__`@abstractmethod` を記述して定義__ する
* `disable_shortcutkeys`、`create_button` と `set_button_status` は、GUI の処理を行う上で、どの GUI でも __同じ処理を行う、あると便利なメソッド__ なので定義した。__具体的な処理を記述__ するので、抽象メソッドではなく、__通常のメソッドとして定義__ する
* `draw_board` や `draw_mark` など、〇× ゲームの GUI に特化したメソッドは定義しない

なお、`disable_shortcutkeys`、`create_button`、`set_button_status` は、Figure でキー入力操作を行わない場合や、ボタンを表示しない場合などでは必要がない処理なので、__GUI に必須となる処理ではありません__。そのため、基底クラスに定義するのではなく、__別のクラスで定義して委譲を使ったほうが良い__ と考える人がいるかもしれません。筆者もその点については迷ったのですが、今回は基底クラスに組み込むことにしました。委譲の方が良いと思った方は、別のクラスを定義して委譲するように修正して下さい。

なお、下記のプログラムは、JupyterLab のセルではなく、gui.py に記述しました。util.py に記述しようと最初は思っていたのですが、util.py に記述すると marubatsu.py から GUI クラスをインポートした時に[循環インポート](https://qiita.com/ysgeso/items/64de676735836b963712#%E5%BE%AA%E7%92%B0%E3%82%A4%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%88%E3%81%AE%E3%82%A8%E3%83%A9%E3%83%BC%E3%81%AE%E6%84%8F%E5%91%B3%E3%81%A8%E3%81%9D%E3%81%AE%E5%AF%BE%E5%87%A6%E6%96%B9%E6%B3%95)が発生してしまうので、gui.py に保存しました。

```python
from abc import ABCMeta, abstractmethod
import matplotlib as mlp
import ipywidgets as widgets

class GUI(metaclass=ABCMeta):
    def __init__(self):
        # %matplotlib widget のマジックコマンドを実行する
        get_ipython().run_line_magic('matplotlib', 'widget')
        
        self.disable_shortcutkeys()
        self.create_widgets()
        self.create_event_handler()
        self.display_widgets() 
   
    @abstractmethod
    def create_widgets(self):
            pass
    
    @abstractmethod
    def create_event_handler(self):
        pass
    
    @abstractmethod
    def display_widgets(self):
        pass

    @abstractmethod
    def update_gui(self):
        pass

    @abstractmethod
    def update_widgets_status(self):
        pass

    @staticmethod
    def disable_shortcutkeys():
        attrs = [ "fullscreen", "home", "back", "forward", "pan", "zoom", "save", "help",
                "quit", "quit_all", "grid", "grid_minor", "yscale", "xscale", "copy"]
        for attr in attrs:
            mlp.rcParams[f"keymap.{attr}"] = []     
            
    @staticmethod
    def create_button(description, width):   
        return widgets.Button(
            description=description,
            layout=widgets.Layout(width=f"{width}px"),
            style={"button_color": "lightgreen"},
        )   
        
    @staticmethod
    def set_button_status(button, disabled):   
        button.disabled = disabled
        button.style.button_color = "lightgray" if disabled else "lightgreen"    
```

## Marubatsu_GUI クラスの修正

次に、Marubatsu_GUI クラスを、GUI クラスを基底クラスとして、下記のプログラムのように定義し直します。具体的には、以下の修正を行いました。

* __`class Marubatsu_GUI(GUI):`__ と記述することで、GUI クラスを継承した、__派生クラスとして定義__ する
* __`__init__`__ メソッドの最後で __`super().__init__()`__ を記述することで、〇×ゲームの GUI の処理に必要な処理を行った後で、基底クラスである __GUI クラスの `__init__` メソッドの処理を実行__ するようにする
* `disable_shortcutkeys`、`create_button` と `set_button_status` は基底クラスで定義したので削除する

なお、基底クラスで抽象メソッドとして定義した `update_widget_status` などの関数は、特に変更する必要はありません。

なお、下記のプログラムは、JupyterLab ではなく、marubatsu_new.py に記述しました。

<details><summary><strong>非常に長いのでクリックしてみて下さい。</strong></summary>

```python
class Marubatsu_GUI(GUI):
    """ 〇×ゲームの GUI.

    Attributes:
        インスタンス属性。mb 以外は play メソッドの仮引数と同じ
        mb:
            GUI を表示する Marubatsu ゲームのインスタンス
        ai (list):
            それぞれの手番を誰が担当するかを指定する list
        ai_dict (dict):
            Dropdown で選択できる AI の一覧を表す dict
        seed (int|None):
            乱数の種
        size (int):
            gui が True の場合に描画するゲーム盤の画像のサイズ
    """
    
    def __init__(self, mb, ai_dict=None, seed=None, size=3):
        """ GUI のウィジェットの作成などの初期設定を行う.
        
        Args:
            Marubatsu_GUI の属性と同じ
        """
        
        # JupyterLab からファイルダイアログを開く際に必要な前処理
        root = Tk()
        root.withdraw()
        root.call('wm', 'attributes', '.', '-topmost', True)  

        # save フォルダが存在しない場合は作成する
        if not os.path.exists("save"):
            os.mkdir("save")        
        
        # ai_dict が None の場合は、空の list で置き換える
        if ai_dict is None:
            ai_dict = {}

        self.mb = mb
        self.ai_dict = ai_dict
        self.seed = seed
        self.size = size

        # ai_dict が None の場合は、空の list で置き換える
        if ai_dict is None:
            self.ai_dict = {}
        
        super().__init__()
                   
    def create_dropdown(self):
        """AI を選択する Dropdown を作成する."""
 
        # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
        select_values = []
        # 〇 と × の Dropdown を格納する list
        self.dropdown_list = []
        # ai に代入されている内容を ai_dict に追加する
        for i in range(2):
            # ラベルと項目の値を計算する
            if self.mb.ai[i] is None:
                label = "人間"
                value = "人間"
            else:
                label = self.mb.ai[i].__name__        
                value = self.mb.ai[i]
            # value を select_values に常に登録する
            select_values.append(value)
            # value が ai_values に登録済かどうかを判定する
            if value not in self.ai_dict.values():
                # 項目を登録する
                self.ai_dict[label] = value
        
            # Dropdown の description を計算する
            description = "〇" if i == 0 else "×"
            self.dropdown_list.append(
                widgets.Dropdown(
                    options=self.ai_dict,
                    description=description,
                    layout=widgets.Layout(width="100px"),
                    style={"description_width": "20px"},
                    value=select_values[i],
                )
            )      
               
    def create_figure(self):
        """ゲーム盤を描画する Figure を作成する."""
        
        with plt.ioff():
            self.fig, self.ax = plt.subplots(figsize=[self.size, self.size])
        self.fig.canvas.toolbar_visible = False
        self.fig.canvas.header_visible = False
        self.fig.canvas.footer_visible = False
        self.fig.canvas.resizable = False     
        
    def create_widgets(self):
        """ウィジェットを作成する."""

        # 乱数の種の Checkbox と IntText を作成する
        self.checkbox = widgets.Checkbox(value=self.seed is not None, description="乱数の種",
                                        indent=False, layout=widgets.Layout(width="100px"))
        self.inttext = widgets.IntText(value=0 if self.seed is None else self.seed,
                                    layout=widgets.Layout(width="100px"))   

        # 読み書き、ヘルプのボタンを作成する
        self.load_button = self.create_button("開く", 80)
        self.save_button = self.create_button("保存", 80)
        self.help_button = self.create_button("？", 30)
        
        # AI を選択する Dropdown を作成する
        self.create_dropdown()
        # 変更、リセット、待ったボタンを作成する
        self.change_button = self.create_button("変更", 50)
        self.reset_button = self.create_button("リセット", 80)
        self.undo_button = self.create_button("待った", 60)    
        
        # リプレイのボタンとスライダーを作成する
        self.first_button = self.create_button("<<", 50)
        self.prev_button = self.create_button("<", 50)
        self.next_button = self.create_button(">", 50)
        self.last_button = self.create_button(">>", 50)     
        self.slider = widgets.IntSlider(layout=widgets.Layout(width="200px"))
        # ゲーム盤の画像を表す figure を作成する
        self.create_figure()

        # print による文字列を表示する Output を作成する
        self.output = widgets.Output()         
    
    def display_widgets(self):
        """ ウィジェットを配置して表示する."""

        # 乱数の種のウィジェット、読み書き、ヘルプのボタンを横に配置した HBox を作成する
        hbox1 = widgets.HBox([self.checkbox, self.inttext, self.load_button, 
                            self.save_button, self.help_button])
        # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
        hbox2 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
        # リプレイ機能のボタンを横に配置した HBox を作成する
        hbox3 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider]) 
        # hbox1 ~ hbox3、Figure、Output を縦に配置した VBox を作成し、表示する
        display(widgets.VBox([hbox1, hbox2, hbox3, self.fig.canvas, self.output]))    
            
    def update_widgets_status(self):
        """ウィジェットの状態を更新する."""
        
        self.inttext.disabled = not self.checkbox.value
        self.set_button_status(self.undo_button, self.mb.move_count < 2 or self.mb.move_count != len(self.mb.records) - 1)
        self.set_button_status(self.first_button, self.mb.move_count <= 0)
        self.set_button_status(self.prev_button, self.mb.move_count <= 0)
        self.set_button_status(self.next_button, self.mb.move_count >= len(self.mb.records) - 1)
        self.set_button_status(self.last_button, self.mb.move_count >= len(self.mb.records) - 1)    
        # value 属性よりも先に max 属性に値を代入する必要がある点に注意！
        self.slider.max = len(self.mb.records) - 1
        self.slider.value = self.mb.move_count
       
    def create_event_handler(self):
        """イベントハンドラを定義し、ウィジェットに結び付ける."""
 
        # 乱数の種のチェックボックスのイベントハンドラを定義する
        def on_checkbox_changed(changed):
            self.update_widgets_status()
            
        self.checkbox.observe(on_checkbox_changed, names="value")

        # 開く、保存ボタンのイベントハンドラを定義する
        def on_load_button_clicked(b=None):
            path = filedialog.askopenfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
                                            initialdir="save")
            if path != "":
                with open(path, "rb") as f:
                    data = pickle.load(f)
                    self.mb.records = data["records"]
                    self.mb.ai = data["ai"]
                    change_step(data["move_count"])
                    for i in range(2):
                        value = "人間" if self.mb.ai[i] is None else self.mb.ai[i]
                        self.dropdown_list[i].value = value               
                    if data["seed"] is not None:                   
                        self.checkbox.value = True
                        self.inttext.value = data["seed"]
                    else:
                        self.checkbox.value = False
                        
        def on_save_button_clicked(b=None):
            name = ["人間" if self.mb.ai[i] is None else self.mb.ai[i].__name__
                    for i in range(2)]
            timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
            fname = f"{name[0]} VS {name[1]} {timestr}"
            path = filedialog.asksaveasfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
                                                initialdir="save", initialfile=fname,
                                                defaultextension="mbsav")
            if path != "":
                with open(path, "wb") as f:
                    data = {
                        "records": self.mb.records,
                        "move_count": self.mb.move_count,
                        "ai": self.mb.ai,
                        "seed": self.inttext.value if self.checkbox.value else None
                    }
                    pickle.dump(data, f)
                    
        def on_help_button_clicked(b=None):
            self.output.clear_output()
            with self.output:
                print("""操作説明

    マスの上でクリックすることで着手を行う。
    下記の GUI で操作を行うことができる。
    ()が記載されているものは、キー入力で同じ操作を行うことができることを意味する。
    なお、キー入力の操作は、ゲーム盤をクリックして選択状態にする必要がある。

    乱数の種\tチェックボックスを ON にすると、右のテキストボックスの乱数の種が適用される
    開く(-,L)\tファイルから対戦データを読み込む
    保存(+,S)\tファイルに対戦データを保存する
    ？(*,H)\t\tこの操作説明を表示する
    手番の担当\tメニューからそれぞれの手番の担当を選択する
    \t\tメニューから選択しただけでは担当は変更されず、変更またはリセットボタンによって担当が変更される
    変更\t\tゲームの途中で手番の担当を変更する
    リセット\t手番の担当を変更してゲームをリセットする
    待った(0)\t1つ前の自分の着手をキャンセルする
    <<(↑)\t\t最初の局面に移動する
    <(←)\t\t1手前の局面に移動する
    >(→)\t\t1手後の局面に移動する
    >>(↓)\t\t最後の着手が行われた局面に移動する
    スライダー\t現在の手数を表す。ドラッグすることで任意の手数へ移動する

    手数を移動した場合に、最後の着手が行われた局面でなければ、リプレイモードになる。
    リプレイモード中に着手を行うと、リプレイモードが解除され、その着手が最後の着手になる。""")
                
        self.load_button.on_click(on_load_button_clicked)
        self.save_button.on_click(on_save_button_clicked)
        self.help_button.on_click(on_help_button_clicked)
        
        # 変更ボタンのイベントハンドラを定義する
        def on_change_button_clicked(b):
            for i in range(2):
                self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
            self.mb.play_loop(self)

        # リセットボタンのイベントハンドラを定義する
        def on_reset_button_clicked(b=None):
            # 乱数の種のチェックボックスが ON の場合に、乱数の種の処理を行う
            if self.checkbox.value:
                random.seed(self.inttext.value)
            self.mb.restart()
            self.output.clear_output()
            on_change_button_clicked(b)

        # 待ったボタンのイベントハンドラを定義する
        def on_undo_button_clicked(b=None):
            if self.mb.move_count >= 2 and self.mb.move_count == len(self.mb.records) - 1:
                self.mb.move_count -= 2
                self.mb.records = self.mb.records[0:self.mb.move_count+1]
                self.mb.change_step(self.mb.move_count)
                self.update_gui()
            
        # イベントハンドラをボタンに結びつける
        self.change_button.on_click(on_change_button_clicked)
        self.reset_button.on_click(on_reset_button_clicked)   
        self.undo_button.on_click(on_undo_button_clicked)   
        
        # step 手目の局面に移動する
        def change_step(step):
            self.mb.change_step(step)
            # 描画を更新する
            self.update_gui()        

        def on_first_button_clicked(b=None):
            change_step(0)

        def on_prev_button_clicked(b=None):
            change_step(self.mb.move_count - 1)

        def on_next_button_clicked(b=None):
            change_step(self.mb.move_count + 1)
            
        def on_last_button_clicked(b=None):
            change_step(len(self.mb.records) - 1)

        def on_slider_changed(changed):
            if self.mb.move_count != changed["new"]:
                change_step(changed["new"])
                
        self.first_button.on_click(on_first_button_clicked)
        self.prev_button.on_click(on_prev_button_clicked)
        self.next_button.on_click(on_next_button_clicked)
        self.last_button.on_click(on_last_button_clicked)
        self.slider.observe(on_slider_changed, names="value")
        
        # ゲーム盤の上でマウスを押した場合のイベントハンドラ
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合のみ処理を行う
            if event.inaxes and self.mb.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                with self.output:
                    self.mb.move(x, y)                
                # 次の手番の処理を行うメソッドを呼び出す
                self.mb.play_loop(self)

        # ゲーム盤を選択した状態でキーを押した場合のイベントハンドラ
        def on_key_press(event):
            keymap = {
                "up": on_first_button_clicked,
                "left": on_prev_button_clicked,
                "right": on_next_button_clicked,
                "down": on_last_button_clicked,
                "0": on_undo_button_clicked,
                "enter": on_reset_button_clicked,            
                "-": on_load_button_clicked,            
                "l": on_load_button_clicked,            
                "+": on_save_button_clicked,            
                "s": on_save_button_clicked,            
                "*": on_help_button_clicked,            
                "h": on_help_button_clicked,            
            }
            if event.key in keymap:
                keymap[event.key]()
            else:
                try:
                    num = int(event.key) - 1
                    event.inaxes = True
                    event.xdata = num % 3
                    event.ydata = 2 - (num // 3)
                    on_mouse_down(event)
                except:
                    pass
                
        # fig の画像イベントハンドラを結び付ける
        self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
        self.fig.canvas.mpl_connect("key_press_event", on_key_press)        

    @staticmethod
    def draw_mark(ax, x:int, y:int, mark:str, color:str="black", lw:float=2):
        """マークを描画する.
        
        (x, y) のマスに、mark で指定したマークの画像を color で指定した色で描画する
        
        Args:
            ax:
                マークを描画する Axes
            x:
                マークを描画する、ゲーム盤の x 座標
            y:
                マークを描画する、ゲーム盤の y 座標
            mark:
                描画するマーク
            color:
                描画するマークの色
            lw:
                描画する図形の線の太さ
        """    
        
        if mark == Marubatsu.CIRCLE:
            circle = patches.Circle([x + 0.5, y + 0.5], 0.35, ec=color, fill=False, lw=lw)
            ax.add_artist(circle)
        elif mark == Marubatsu.CROSS:
            ax.plot([x + 0.15, x + 0.85], [y + 0.15, y + 0.85], c=color, lw=lw)
            ax.plot([x + 0.15, x + 0.85], [y + 0.85, y + 0.15], c=color, lw=lw)
    
    @staticmethod
    def draw_board(ax, mb:Marubatsu, show_result:bool=False, dx:float=0, dy:float=0, lw:float=2):
        """ゲーム盤を描画する
        
        Args:
            ax:
                ゲーム盤を描画する Axes
            mb:
                ゲーム盤を表す Marubatsu クラスのインスタンス
            show_result:
                決着がついている場合に、背景色を変えて描画する
            dx:
                描画するゲーム盤の左上の点の Axes の x 座標
            dy:
                描画するゲーム盤の左上の点の Axes の y 座標
            lw:
                描画する図形の線の太さ
        """
        
        # 結果によってゲーム盤の背景色を変更する
        if show_result:
            if mb.status == Marubatsu.PLAYING:
                bgcolor = "white"
            elif mb.status == Marubatsu.CIRCLE:
                bgcolor = "lightcyan"
            elif mb.status == Marubatsu.CROSS:
                bgcolor = "lavenderblush"
            else:
                bgcolor = "lightyellow"
            rect = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
                                    height=mb.BOARD_SIZE, fc=bgcolor)
            ax.add_patch(rect)
        
        # ゲーム盤の枠を描画する
        for i in range(1, mb.BOARD_SIZE):
            ax.plot([dx, dx + mb.BOARD_SIZE], [dy + i, dy + i], c="k", lw=lw) # 横方向の枠線
            ax.plot([dx + i, dx + i], [dy, dy + mb.BOARD_SIZE], c="k", lw=lw) # 縦方向の枠線

        # ゲーム盤のマークを描画する
        for y in range(mb.BOARD_SIZE):
            for x in range(mb.BOARD_SIZE):
                color = "red" if (x, y) == mb.last_move else "black"
                Marubatsu_GUI.draw_mark(ax, dx + x, dy + y, mb.board[x][y], color, lw=lw)
                    
    def update_gui(self):
        """GUI の表示や設定を更新する"""
        
        ax = self.ax
        ai = self.mb.ai
        
        # Axes の内容をクリアして、これまでの描画内容を削除する
        ax.clear()
        
        # y 軸を反転させる
        ax.invert_yaxis()
        
        # 枠と目盛りを表示しないようにする
        ax.axis("off")   
        
        # リプレイ中、ゲームの決着がついていた場合は背景色を変更する
        is_replay =  self.mb.move_count < len(self.mb.records) - 1 
        if self.mb.status == Marubatsu.PLAYING:
            facecolor = "lightcyan" if is_replay else "white"
        else:
            facecolor = "lightyellow"

        ax.figure.set_facecolor(facecolor)
            
        # 上部のメッセージを描画する
        # 対戦カードの文字列を計算する
        names = []
        for i in range(2):
            names.append("人間" if ai[i] is None else ai[i].__name__)
        ax.text(0, 3.5, f"{names[0]}　VS　{names[1]}", fontsize=20)   
        
        # ゲームの決着がついていない場合は、手番を表示する
        if self.mb.status == Marubatsu.PLAYING:
            text = "Turn " + self.mb.turn
        # 引き分けの場合
        elif self.mb.status == Marubatsu.DRAW:
            text = "Draw game"
        # 決着がついていれば勝者を表示する
        else:
            text = "Winner " + self.mb.status
        # リプレイ中の場合は "Replay" を表示する
        if is_replay:
            text += " Replay"
        ax.text(0, -0.2, text, fontsize=20)
        
        self.draw_board(ax, self.mb)
        
        self.update_widgets_status() 
```
</details>

<details><summary><strong>主な修正箇所</strong></summary>

```diff_python
-class Marubatsu_GUI():
+class Marubatsu_GUI(GUI):
    def __init__(self, mb, ai_dict=None, seed=None, size=3):
        # JupyterLab からファイルダイアログを開く際に必要な前処理
        root = Tk()
        root.withdraw()
        root.call('wm', 'attributes', '.', '-topmost', True)  

        # save フォルダが存在しない場合は作成する
        if not os.path.exists("save"):
            os.mkdir("save")        
        
        # ai_dict が None の場合は、空の list で置き換える
        if ai_dict is None:
            ai_dict = {}

        self.mb = mb
        self.ai_dict = ai_dict
        self.seed = seed
        self.size = size

        # ai_dict が None の場合は、空の list で置き換える
        if ai_dict is None:
            self.ai_dict = {}
        
        # %matplotlib widget のマジックコマンドを実行する
-       get_ipython().run_line_magic('matplotlib', 'widget')
        
-       self.disable_shortcutkeys()
-       self.create_widgets()
-       self.create_event_handler()
-       self.display_widgets() 

+  　   super().__init__()
以下略
```
</details>

行結果は省略しますが、GUI を gui.py に、上記の Marubatsu_GUI を marubatsu.py に記述し、下記のプログラムを実行することで、修正した Marubatsu_GUI が正しく動作することを確認することができます。なお、`gui_play` は、marubatsu.py から Marubatsu クラスをインポートするので、今回は利用することはできません。

```python
from marubatsu_new import Marubatsu

mb = Marubatsu()
mb.play(ai=[None, None], gui=True)
```

# 今回の記事のまとめ

今回の記事では、委譲と継承について説明し、基底クラスとして GUI クラスを作成し、Marubatsu_GUI クラスを継承を使って定義しなおしました。

次回の記事では、今回の記事で定義した GUI クラスを継承することで、ゲーム木の視覚化を行う GUI を作成します。

# 本記事で入力したプログラム

以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。

https://github.com/ysgeso/marubatsu/blob/master/093/marubatsu.ipynb

以下のリンクは、今回の記事で更新した marubatsu.py です。

https://github.com/ysgeso/marubatsu/blob/master/093/marubatsu_new.py

以下のリンクは、今回の記事で作成した gui.py です。

https://github.com/ysgeso/marubatsu/blob/master/093/gui.py

# 次回の記事

https://qiita.com/ysgeso/items/0335cc42055729d0fac3

| 更新日時 | 更新内容 |
|:--|:--|
| 2024/09/05 | [オーバーライドの目的と基底クラスのメソッドの呼び出しのノート](#オーバーライドの目的と基底クラスのメソッドの呼び出し) に `super` を クラスの定義の外で記述されたメソッドで利用できることを追記しました |

[^1]:作者であっても、時間がたてば見つけることが難しくなることが多いでしょう
[^2]:A is B だと、A と B は同値（全く同じもの）という意味が強くなりますが、A is a B とすることで、A は B の一種であるという意味になります
[^3]:行っても良いというだけで、__行うべきであるという意味ではない__ 点に注意して下さい