Pythonで〇×ゲームのAIを一から作成する　その82　self の意味の混同に由来する様々なエラー

# 目次と前回の記事

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

https://qiita.com/ysgeso/items/05ac74365dad096dd28f

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

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

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

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

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

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

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

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

# クラスによる GUI の機能の分離（続き）

前回の記事では、__`Marubatsu_GUI` クラスを定義__ することで、__`Marubatsu` クラスから GUI の機能を分離__ する作業を開始しました。

前回の記事では比較的スムーズに分離の作業が行えましたが、今回の記事では、作業を行うたびに様々な問題が発生し、それを修正するという、かなり面倒な作業を何度も行います。

もちろん、プログラミングの技能が上達すれば、発生する可能性のある問題をあらかじめ考慮しながら作業を行うことで、今回の記事よりも効率よく作業を行うことができるようになりますが、そのためには実際に面倒な作業を体験することが重要だと思います。また、今回の記事のような面倒な作業は、プログラムの構造を変更するような修正を行う際に、実際に行う必要がある場合がよくあるので紹介することにします。

# ゲーム盤の画像を描画するの関数の定義

[前回の記事](https://qiita.com/ysgeso/items/05ac74365dad096dd28f#%E3%82%B2%E3%83%BC%E3%83%A0%E7%9B%A4%E3%82%92%E8%A1%A8%E3%81%99-figure-%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B%E9%96%A2%E6%95%B0)の最後で、ゲーム盤を描画する Figure を作成したので、次は __ゲーム盤の画像を描画__ する処理を行っていた `Marubatsu` クラスの `draw_board` を __`Marubatsu_GUI` クラスのメソッドとして定義し直す__ ことにします。

## `set_button_status` と `update_widgets_status` メソッドの定義

[前回の記事](https://qiita.com/ysgeso/items/05ac74365dad096dd28f#marubatsu_gui-%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E8%A8%98%E8%BF%B0%E3%81%AE%E6%96%B9%E9%87%9D)で、「必要がない限り、ローカル変数やローカル関数を使わずに、__属性とメソッドを利用する__」という方針をとることにしたので、__`draw_board` のローカル関数__ として定義されていた __`set_button_status`__ と __`update_widgets_status`__ を、下記のプログラムのように __`Marubatsu_GUI` のメソッドとして定義__ することにします。

* __3 ~ 7 行目__：__`set_button_status`__ は、__`Marubatsu_GUI` クラスの情報を利用しない__ ので、__`@staticmethod`__ を使って __[静的メソッド](https://qiita.com/ysgeso/items/8ac83f1b50b2706b8319#%E9%9D%99%E7%9A%84%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%A8%E3%81%97%E3%81%A6%E3%81%AE-draw_mark-%E3%81%AE%E5%AE%9A%E7%BE%A9)として定義__ する。なお、この部分を通常のメソッドとして定義しても問題はない
* __12 ~ 17 行目__：`update_widgets_status` を `Marubatsu_GUI` のメソッドとして定義する
```python
 1  from marubatsu import Marubatsu_GUI
 2
 3  @staticmethod
 4  # ボタンのウィジェットの状態を設定する
 5  def set_button_status(button, disabled):
 6      button.disabled = disabled
 7      button.style.button_color = "lightgray" if disabled else "lightgreen"
 8
 9  Marubatsu_GUI.set_button_status = set_button_status
10
11  # ウィジェットの状態を更新する        
12  def update_widgets_status(self):
13      # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
14      set_button_status(self.first_button, self.move_count <= 0)
15      set_button_status(self.prev_button, self.move_count <= 0)
16      set_button_status(self.next_button, self.move_count >= len(self.board_records) - 1)
17      set_button_status(self.last_button, self.move_count >= len(self.board_records) - 1)
18
19  Marubatsu_GUI.update_widgets_status = update_widgets_status
```

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

```python
from marubatsu import Marubatsu_GUI

@staticmethod
# ボタンのウィジェットの状態を設定する
def set_button_status(button, disabled):
    button.disabled = disabled
    button.style.button_color = "lightgray" if disabled else "lightgreen"

Marubatsu_GUI.set_button_status = set_button_status

# ウィジェットの状態を更新する        
def update_widgets_status(self):
    # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
    set_button_status(self.first_button, self.move_count <= 0)
    set_button_status(self.prev_button, self.move_count <= 0)
    set_button_status(self.next_button, self.move_count >= len(self.board_records) - 1)
    set_button_status(self.last_button, self.move_count >= len(self.board_records) - 1)

Marubatsu_GUI.update_widgets_status = update_widgets_status
```
</details>

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

```diff_python
from marubatsu import Marubatsu_GUI

+@staticmethod
# ボタンのウィジェットの状態を設定する
def set_button_status(button, disabled):
    button.disabled = disabled
    button.style.button_color = "lightgray" if disabled else "lightgreen"

Marubatsu_GUI.set_button_status = set_button_status

# ウィジェットの状態を更新する        
-def update_widgets_status():
+def update_widgets_status(self):
    # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
    set_button_status(self.first_button, self.move_count <= 0)
    set_button_status(self.prev_button, self.move_count <= 0)
    set_button_status(self.next_button, self.move_count >= len(self.board_records) - 1)
    set_button_status(self.last_button, self.move_count >= len(self.board_records) - 1)

Marubatsu_GUI.update_widgets_status = update_widgets_status
```
</details>

## `draw_board` の定義

次に、__`draw_board`__ を __`marubatsu_GUI` のメソッドとして定義__ する必要がありますが、その際に __下記の点に注意__ する必要があります。

* __`Marubatsu`__ クラスの `draw_board` の __`self`__ は、__`Marubatsu` クラスのインスタンス__ である
* __`Marubatsu_GUI`__ クラスの `draw_board` の __`self`__ は、__`Marubatsu_GUI` クラスのインスタンス__ である
* __`Marubatsu_GUI`__ クラスのインスタンスの __`mb` という属性__ に、__`Marubatsu` クラスのインスタンスが代入__ されている
* 従って、__`Marubatsu`__ クラスの `draw_board` を __`Marubatsu_GUI` クラスの `draw_board` として定義し直す__ 場合は、__`self` を `self.mb` に修正する__ 必要がある

### 置換の機能の問題点

これまでの記事では、__名前を変更する際__ に、変更箇所が多くなかったので、__変更する場所を自分で見つけて修正__ を行っていましたが、上記の修正は __変更箇所が多い__ ので、変更箇所を __自分で探して変更するのは大変__ なだけでなく、__間違えやすい__ という問題があります。

一般的な文章を編集するソフトでは、`draw_board` の `self` を `self.mb` に修正するような、__置換の作業を大量に行う際__ に、__置換の機能を使うのが一般的__ です。VSCode にも置換を行う機能がありますが、下記のような問題があるので、この後で説明する __シンボル名の変更__ の機能を使ったほうが良いでしょう。

* 置換の機能は、__プログラムの文脈を考慮せず__ に、指定した文字列を置換の対象とする
* そのため、置換の機能を利用する際は、置換を行う文字列を __一つ一つ検索__ して探し、その文字列を __置換しても良いかどうかを確認しながら行う必要__ がある

具体例を示します。下記のプログラムで、グローバル変数 `i` の名前を `j` に変更する必要が生じたと思ってください。

```python
i = 1         # この i はグローバル変数

def f():
    i = 1     # この i はローカル変数
    print(i)  # この i はローカル変数
    
print(i)      # この i はグローバル変数
```

VSCode の置換の機能を使ってこの変更を行う場合は、下記のような手順で行います。

1. 「編集」メニューの「置換」を選択するか、Ctrl + H を押すと下図のパネルが表示される<br><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/dc0571af-14a7-b43d-efb9-23c4ba17f384.png" width="200">

2. 下図のように、上のテキストボックスに `i` を、下のテキストボックスに `j` を入力する。なお、「1/26件」はファイルの中に 26 か所の `i` が検索され、その中の 1 つ目の `i` が選択状態になっていることを表す<br><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/27e8edd2-bac5-c5fe-dcc9-f0098e0deec7.png" width="200">

3. 下図のようにセルの中の `i` が検索されて色がついて表示される<br><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/7bde6e14-30ac-fd8a-888e-f0f7078e992c.png" width="200">

4. 下図左の赤丸の「すべて置換」ボタンをクリックすると、このセルだけでなく、VSCode で編集中のファイルの __すべての `i` が `j` に置換されてしまう__ のでこの場合は __行ってはならない__。例えば、上図のプログラムの場合は、下図右のようにローカル変数 `i`、`print` の中の `i`、コメントの中の `i` までもが `j` に変換されてしまうため、__うまくいかない__<br><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/442ad8ef-0b55-5358-b9e8-68f44062a197.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/91f6e067-9da9-e6a0-2dbf-7a4c52582f24.png" width="200">

5. 下図左の赤丸の矢印のボタンをクリックすることで、`i` が順番に検索されて選択状態になるので、__置換する必要がある `i` を検索状態にする__。下図右は、プログラムが記述されたセルの、最初の `i` を選択状態（灰色で表示される）にした図である<br><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/17857d8d-8c42-a500-bf7f-1051b67ff77f.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/5405df25-aec7-7617-2914-7e71b11c429d.png" width="200">

6. 下図左の赤丸の「置換」ボタンをクリックすると、下図右のように、__選択状態の `i` だけが `j` に置換される__<br><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/01fc9bff-1541-1023-9524-6c3245ae0ef4.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/846d01db-cba2-2bfa-f8c9-335e97707774.png" width="200">

7. 上記の作業を、__置換する必要があるすべての `i` に対して行う__

置換の作業は、上記の例のように、__置換する必要がある `i`__ と __置換してはいけない `i`__ が __混在する__ ような場合は、__かなり面倒__ で、__間違えやすい__ 作業が必要になります。

### VSCode のシンボル名の変更の使い方

VSCode には、プログラムの文法から、__関連する名前だけを置換__ するという、「__シンボル名の変更__」[^1]という機能があります。上記のプログラムのグローバール変数 `i` を `j` に変更する作業は、シンボル名の変更を使って下記の手順で __簡単に行うことができます__。

1. 変更するグローバル変数 `i` をマウスでドラッグして __選択状態にする__
2. マウスの右ボタンを押して表示される下図のメニューから「シンボル名の変更」を選択するか、F2 キーを押す<br><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/12c4b2d7-f620-85a5-06dc-a5e6767b8230.png" width="200">

3. 下図のテキストボックスが表示されるので `j` を入力してエンターキーを押す<br><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/fa6f5895-0284-e821-68b5-d220a86e2da5.png" width="200">

4. 下図のように、__グローバル変数 `i` のみが `j` に変更される__<br><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3b817496-9f7f-bab0-ee4f-f3e36162cc9a.png" width="200">

### シンボル名の変更の注意点

シンボル名の変更は、__プログラムの文法上__ での __同じ意味の名前だけが置換__ される非常に便利な機能ですが、下記の点に注意して下さい

* __コメントの内容__ は、__プログラムで処理を行う内容ではない__ ので、__変更されない__。実際に、上図のように、コメントの `i` は変更されない
* 具体例はこの後で紹介するが、__変更してはいけない名前が変更される場合がある__
* シンボル名の変更は、文法的に同じ意味を持つ名前を変更するので、下記のプログラムの `a` のように、__文法的に正しくない名前__（`a` はグローバル変数として存在せず、ローカル変数としても定義されていない）を __変換することはできない__

```python
b = 0

def f():
    print(a)
    print(a * 2)
```

* 実際に上記の `a` をシンボル名の変更で変更しようとすると、下図のように名前を変更できない（can`t be renamed）という意味のメッセージが表示される<br><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/a0499ffe-7220-ac3f-02d3-aebdd6de8fe2.png" width="200">

:::note
上記の `a` の、グローバル変数 `b` への変更は、下記の手順で行うことができます。

* 下記のプログラムの 4 行目のように、`a = 0` を記述して `a` を __文法的に正しいローカル変数にする__

```python
1  b = 0
2
3  def f():
4      a = 0
5      print(a)
6      print(a * 2)
```

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

```python
b = 0

def f():
    a = 0
    print(a)
    print(a * 2)
```
</details>

* シンボル名の変更で `a` を `b` に変更し、下図のようなプログラムにする

```python
1  b = 0
2
3  def f():
4      b = 0
5      print(b)
6      print(b * 2)
```

* 文法的に正しくするために追加した __4 行目のプログラムを削除する__
:::

### `draw_board` の定義

`Marubatsu_GUI` のメソッドとして `draw_board` を定義する際に、下記の作業を行えば良いと思った人がいるかもしれません。

* `Marubatsu` クラスの `draw_board` の定義をそのままコピーする 
* ローカル関数 `set_button_status` と `update_widgets_status` を削除する
* シンボル名の変更を使って `self` を `self.mb` に変更する

しかし、上記の作業を行うと、下記のプログラムのような修正が行われ、実行すると実行結果のようなエラーが発生します。このエラーの原因について少し考えてみて下さい。なお、シンボル名の変更による修正箇所は自動的に行われるので、修正箇所は省略します。

```python
def draw_board(self.mb, ax, ai):
    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()
    
    # y 軸を反転させる
    ax.invert_yaxis()
    
    # 枠と目盛りを表示しないようにする
    ax.axis("off")   
    
    # ゲームの決着がついていた場合は背景色を
    facecolor = "white" if self.mb.status == Marubatsu.PLAYING else "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
    ax.text(0, -0.2, text, fontsize=20)
    
    # ゲーム盤の枠を描画する
    for i in range(1, self.mb.BOARD_SIZE):
        ax.plot([0, self.mb.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
        ax.plot([i, i], [0, self.mb.BOARD_SIZE], c="k") # 縦方向の枠線   

    # ゲーム盤のマークを描画する
    for y in range(self.mb.BOARD_SIZE):
        for x in range(self.mb.BOARD_SIZE):
            color = "red" if (x, y) == self.mb.last_move else "black"
            self.mb.draw_mark(ax, x, y, self.mb.board[x][y], color)            

    update_widgets_status()   
    
Marubatsu_GUI.draw_board = draw_board
```

実行結果

```
  Cell In[4],   line 1
    def draw_board(self.mb, ax, ai):
                       ^
SyntaxError: invalid syntax
```

### エラーの検証と修正

エラーメッセージから、1 行目の `draw_board` の定義の __仮引数の部分に `self.mb` が記述される__ という、__文法エラー（syntax error）が発生__ していることがわかります。これは、シンボル名の変更で `self` を `self.mb` に変更した際に、__変更してはいけない `draw_board` の仮引数 `self` までもが変更__ されてしまったことが原因です。

上記で行ったシンボル名の変更の機能に不具合があったのではないかと思う人がいるかもしれませんが、そうではありません。__シンボル名の変更__ が行う処理は、__文法的に同じ意味を持つ名前を変更__ するという処理です。`draw_board` の __仮引数 の `self`__ と __ブロックの中の `self`__ は、__文法的には同じ意味を持つ__ ので、__シンボル名の変更__ によって両方が `self.mb` に変更されるのは、__正しい処理__ です。

上記のような間違った変換が行われた原因は、`draw_board` の __`self` の意味__ が `Marubatsu` クラスと `Marubatsu_GUI` クラスで下記の表のように __異なるから__ です。

|  | `Marubatsu` クラス | `Marubatsu_GUI` クラス |
|:--|:--|:--|
| __仮引数の `self`__  | `Marubatsu` のインスタンス  | `Marubatsu_GUI` のインスタンス  |
| __ブロックの中の `self`__  |  `Marubatsu` のインスタンス | `Marubatsu` のインスタンス  |

上記の表のように、__`Marubatsu`__ クラスの `draw_board` メソッド内の __`self`__ は __すべて `Marubatsu` クラスのインスタンス__ ですが、__`Marubatsu_GUI`__ クラスの `draw_board` メソッド内の __`self`__ は、__仮引数とブロックの中で異なる意味__ を持ちます。そのような違いがあるにも関わらず、`Marubatsu` クラスの `draw_board` をそのままコピーして `Marubatsu_GUI` クラスの `draw_board` として定義したため、__文法上__ の仮引数 `self` の意味と、__実際__ の仮引数`self` の意味が __食い違う__ という状況が発生することになります。これがエラーの原因です。

:::note warn
このように、あるクラスのメソッドを、別のクラスのメソッドとして定義し直す際に、__異なる意味を持つ `self` が混在__ する場合があります。そのような場合は、__シンボル名の変更__ によって `self` を変更した際に、__変更してはいけない__ `self` がまでもが __変更されてしまう場合がある__ という点に注意して下さい。

__Python__ では __慣習的に `self` を、クラスのインスタンスを代入する仮引数の名前として利用します__ が、そのせいで、__`self` という名前の意味を混同することによるエラーが発生__ する場合が良くあります。実際に、この後で発生するエラーの多くは `self` に関するものです。

なお、このような、__異なるものに対して同じ名前が付けられてしまう__ という現象は、`self` 以外でも、__プログラムの修正__ による __バグの原因__ として __頻繁に発生する__ ので注意が必要です。
:::

この問題は、下記のプログラムのように、`draw_board` の仮引数を `self` にすることで修正することができます。

```python
def draw_board(self, ax, ai):
元と同じなので省略
    
Marubatsu_GUI.draw_board = draw_board
```

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

```python
def draw_board(self, ax, ai):
    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()
    
    # y 軸を反転させる
    ax.invert_yaxis()
    
    # 枠と目盛りを表示しないようにする
    ax.axis("off")   
    
    # ゲームの決着がついていた場合は背景色を
    facecolor = "white" if self.mb.status == Marubatsu.PLAYING else "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
    ax.text(0, -0.2, text, fontsize=20)
    
    # ゲーム盤の枠を描画する
    for i in range(1, self.mb.BOARD_SIZE):
        ax.plot([0, self.mb.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
        ax.plot([i, i], [0, self.mb.BOARD_SIZE], c="k") # 縦方向の枠線   

    # ゲーム盤のマークを描画する
    for y in range(self.mb.BOARD_SIZE):
        for x in range(self.mb.BOARD_SIZE):
            color = "red" if (x, y) == self.mb.last_move else "black"
            self.mb.draw_mark(ax, x, y, self.mb.board[x][y], color)            

    update_widgets_status()   
    
Marubatsu_GUI.draw_board = draw_board
```
</details>

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

```diff_python
-def draw_board(self.mb, ax, ai):
+def draw_board(self, ax, ai):
元と同じなので省略
    
Marubatsu_GUI.draw_board = draw_board
```
</details>

### `draw_board` メソッドの処理の確認

上記で定義した `draw_board` メソッドが正しく動作するかどうかを、下記のプログラムで確認することにします。実行結果のように、ゲーム盤が描画されるようになりますが、同時にエラーが発生することがわかります。このエラーの原因について少し考えてみて下さい。

* __4 行目__：`Marubatsu_GUI` のインスタンスを作成し、`mb_gui` に代入する
* __5 行目__：`Marubatsu_GUI` の `draw_board` メソッドを呼び出して、ゲーム盤を描画する。`draw_board` の仮引数 `ax` と `ai` に代入する値は、`mb_gui` の属性に代入されるので、それを実引数に記述する

```python
from marubatsu import Marubatsu

mb = Marubatsu()
mb_gui = Marubatsu_GUI(mb, ai=[None, None])
mb_gui.draw_board(mb_gui.ax, mb_gui.ai)
```

実行結果（上部のボタンなどのウィジェットの画像は省略します）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/590d9d32-d51c-d128-b24b-91f72146e4ac.png" width="150">

```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[6], line 5
      3 mb = Marubatsu()
      4 mb_gui = Marubatsu_GUI(mb, ai=[None, None])
----> 5 mb_gui.draw_board(mb_gui.ax, mb_gui.ai)

Cell In[5], line 44
     41         color = "red" if (x, y) == self.mb.last_move else "black"
     42         self.mb.draw_mark(ax, x, y, self.mb.board[x][y], color)            
---> 44 update_widgets_status()

TypeError: update_widgets_status() missing 1 required positional argument: 'self'
```

### エラーの検証と修正

エラーメッセージから、__`draw_board` 内__ で __`update_widgets_status` を呼び出す際__ に、__位置引数__（positional argument） `self` に対応する __実引数が記述されていない__ ことが原因であることがわかります。また、このようなエラーが発生した原因は以下の通りです。

* __`Marubatsu`__ クラスの `draw_board` 内では、__`update_widgets_status`__ は、__ローカル関数として定義__ されていたので、`update_widgets_status()` のように記述して呼び出していた
* __`Marubatsu_GUI`__ クラスでは、先程 __`update_widgets_status`__ を __メソッドとして定義__ したので、呼び出す際には、__`self.update_widgets_status()`__ のように、`Marubatsu_GUI` クラスの __インスタンスから呼び出す必要がある__

従って、`draw_board` を下記のプログラムの 3 行目のように修正することで、この問題を解決することができます。

```python
def draw_board(self, ax, ai):
元と同じなので省略      
    self.update_widgets_status()   
    
Marubatsu_GUI.draw_board = draw_board
```

<details><summary><strong>プログラム全体</strong></summary>

```python
def draw_board(self, ax, ai):
    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()
    
    # y 軸を反転させる
    ax.invert_yaxis()
    
    # 枠と目盛りを表示しないようにする
    ax.axis("off")   
    
    # ゲームの決着がついていた場合は背景色を
    facecolor = "white" if self.mb.status == Marubatsu.PLAYING else "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
    ax.text(0, -0.2, text, fontsize=20)
    
    # ゲーム盤の枠を描画する
    for i in range(1, self.mb.BOARD_SIZE):
        ax.plot([0, self.mb.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
        ax.plot([i, i], [0, self.mb.BOARD_SIZE], c="k") # 縦方向の枠線   

    # ゲーム盤のマークを描画する
    for y in range(self.mb.BOARD_SIZE):
        for x in range(self.mb.BOARD_SIZE):
            color = "red" if (x, y) == self.mb.last_move else "black"
            self.mb.draw_mark(ax, x, y, self.mb.board[x][y], color)            

    self.update_widgets_status()   
    
Marubatsu_GUI.draw_board = draw_board
```
</details>

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

```diff_python
def draw_board(self, ax, ai):
元と同じなので省略      
-   update_widgets_status()   
+   self.update_widgets_status()   
    
Marubatsu_GUI.draw_board = draw_board
```
</details>

上記の修正後に、下記のプログラムを実行すると、実行結果のように、別のエラーが発生します。このエラーの原因について少し考えてみて下さい。なお、表示されるボタンや画像は先ほどと同じなので省略します。

```python
mb_gui = Marubatsu_GUI(mb, ai=[None, None])
mb_gui.draw_board(mb_gui.ax, mb_gui.ai)
```

実行結果

```
Cell In[1], line 14
     12 def update_widgets_status(self):
     13     # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
---> 14     set_button_status(self.first_button, self.move_count <= 0)
     15     set_button_status(self.prev_button, self.move_count <= 0)
     16     set_button_status(self.next_button, self.move_count >= len(self.board_records) - 1)

AttributeError: 'Marubatsu_GUI' object has no attribute 'move_count'
```

### 新たなエラーの検証と修正

エラーメッセージから、__`update_widgets_status`__ メソッド内で、__`self.move_count`__ の処理を行う際に、__`Marubatsu_GUI`__ クラスのインスタンスである __`self`__ に __`move_count` という属性が存在していない__ ことが原因であることがわかります。

__`Marubatsu`__ クラスの __`draw_board`__ メソッド内で __ローカル関数__ として __`update_widgets_status` を定義__ していた際には、__`self`__ は __`Marubatsu` クラスのインスタンス__ であったので、`self.move_count` はエラーにはなりませんでした。

一方、__`Marubatsu_GUI`__ クラスの __メソッド__ として __`update_widgets_status` を定義__ した の場合は、__`self`__ は __`Marubatsu_GUI` のインスタンス__ を表します。

このように、`update_widgets_status` 内の __`self` の意味が異なる__ にも関わらず、__`draw_board`__ メソッドの __ローカル関数__ を __そのままコピーして `update_widgets_status` を定義__ してしまったことがこの問題の原因です。

:::note warn
このエラーの原因も、先程と同様に、`self` の意味が異なる点です。
:::

ところで、`self` の意味が変化したにもかかわらず、__`update_widgets_status`__ 内の `self.first_button` の部分でエラーが発生しない点が気になった人はいないでしょうか？`Marubatsu_GUI` クラスを作成する前は、< ボタンのウィジェットは `play` メソッド内の下記のプログラムで、__`Marubatsu` クラスの属性に代入__ していました。そのため、__`self`__ に __`Marubatsu` クラスのインスタンス__ が代入されたローカル関数の __`update_widgets_status`__ 内では __`self.first_button`__ に __< ボタンのウィジェットが代入__ されています。

```python
self.first_button = create_button("<<", 100)
```            

一方、__`Marubatsu_GUI`__ クラスでは、< ボタンのウィジェットは __`create_widgets`__ メソッド内の上記のプログラムで、__`Marubatsu_GUI` クラスの属性に代入__ しています。そのため、__`self`__ に __`Marubatsu_GUI` クラスのインスタンス__ が代入された __`update_widgets_status`__ メソッド内でも __`self.first_button`__ に __< ボタンのウィジェットが代入__ されています。これが、`self.first_button` のほうではエラーが発生しない理由です。

上記をまとめると、< ボタンのウィジェットの __代入先__ を __`Marubatsu`__ クラスの属性から、__`Marubatsu_GUI`__ クラスの __属性に変更__ したため、__`update_widgets_status`__ の `self` の意味が変わっても `self.first_button` に < ボタンのウィジェットが代入されるということです。

非常にわかりづらいと思いますので、`Marubatsu` クラスと `Marubatsu_GUI` クラスでの、`update_widgets_status` の性質の違いを表にまとめます。

|  | `Marubatsu` クラス | `Marubatsu_GUI` クラス |
|:--|:--|:--|
| 定義  |  `draw_board` のローカル関数として定義 | メソッドとして定義  |
| `self` の意味  | `Marubatsu` クラスのインスタンス  | `Marubatsu_GUI` クラスのインスタンス  |
| ボタンの代入先  |  `Marubatsu` クラスの属性 | `Marubatsu_GUI` クラスの属性  |
| `move_count` の代入先  | `Marubatsu` クラスの属性  | `Marubatsu` クラスの属性  |

上記の表からわかるように、`Marubatsu_GUI` クラスの `update_widgets_status` メソッド内では、ボタンの代入先と、`move_count` などの代入先は、__異なるクラスの属性に代入__ されています。それにも関わらず、__どちらも__ `self.first_button` や `self.move_count` のように、__`Marubatsu_GUI` クラスの属性としてそれらを記述__ している点がエラーの原因です。

従って、この問題は下記のプログラムのように、`update_widgets_status` を修正することで解決することができます。

* __4 ~ 7 行目__：`Marubatsu` クラスの属性を参照する必要がある `move_count` と `board_records` の先頭を `self.` から `self.mb.` に修正する

```python
1  # ウィジェットの状態を更新する        
2  def update_widgets_status(self):
3      # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
4      set_button_status(self.first_button, self.mb.move_count <= 0)
5      set_button_status(self.prev_button, self.mb.move_count <= 0)
6      set_button_status(self.next_button, self.mb.move_count >= len(self.mb.board_records) - 1)
7      set_button_status(self.last_button, self.mb.move_count >= len(self.mb.board_records) - 1)
8
9  Marubatsu_GUI.update_widgets_status = update_widgets_status
```

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

```python
# ウィジェットの状態を更新する        
def update_widgets_status(self):
    # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
    set_button_status(self.first_button, self.mb.move_count <= 0)
    set_button_status(self.prev_button, self.mb.move_count <= 0)
    set_button_status(self.next_button, self.mb.move_count >= len(self.mb.board_records) - 1)
    set_button_status(self.last_button, self.mb.move_count >= len(self.mb.board_records) - 1)

Marubatsu_GUI.update_widgets_status = update_widgets_status
```
</details>

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

```diff_python
# ウィジェットの状態を更新する        
def update_widgets_status(self):
    # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
-   set_button_status(self.first_button, self.move_count <= 0)
+   set_button_status(self.first_button, self.mb.move_count <= 0)
-   set_button_status(self.prev_button, self.move_count <= 0)
+   set_button_status(self.prev_button, self.mb.move_count <= 0)
-   set_button_status(self.next_button, self.move_count >= len(self.board_records) - 1)
+   set_button_status(self.next_button, self.mb.move_count >= len(self.mb.board_records) - 1)
-   set_button_status(self.last_button, self.move_count >= len(self.board_records) - 1)
+   set_button_status(self.last_button, self.mb.move_count >= len(self.mb.board_records) - 1)

Marubatsu_GUI.update_widgets_status = update_widgets_status
```
</details>

上記の修正後に、下記のプログラムを実行するとエラーが発生しなくなり、実行結果のようにリプレイ機能に関するボタンが灰色で表示されて操作できなくなることが確認できます。

```python
mb_gui = Marubatsu_GUI(mb, ai=[None, None])
mb_gui.draw_board(mb_gui.ax, mb_gui.ai)
```

実行結果（下図は、画像なので操作することはできません）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/e09e6321-99f8-1dfc-174f-c8eb22807d4b.png" width="250">

### `draw_board` メソッドの改良

`draw_board` メソッドを呼び出す際に、下記のプログラムのように、__実引数に `mb_gui.ax`、`mb_gui.ai` を記述__ していますが、__`draw_board`__ の仮引数 __`self`__ に __`mb_gui` が代入される__ ので、`mb_gui.ax` や `mb_gui.ai` の値は、`draw_board` の中で `self.ax` と `self.ai` と記述して参照することができます。そのため、__`draw_board` の仮引数 `ax` と `ai` は必要がありません__。

```python
mb_gui.draw_board(mb_gui.ax, mb_gui.ai)
```

そこで、`draw_board` から __仮引数 `ax` と `ai` を削除する__ ことにします。その際に、`draw_board` 内の __`ax` と `ai`__ を __`self.ax` と `self.ai` に修正__ する必要がありますが、__仮引数 `ax` と `ai` を削除した後__ で __シンボル名の変更__ で __`ax` を `self.ax` に変更__ しようとしても、下図のような表示が行われて __変更することができません__。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/9473e417-4b7f-ddaf-dc46-9ffca100c867.png" width="250">

これは、__`ax` を仮引数から削除__ したことによって、__`ax`__ が __ローカル変数として定義されていないことになる__ ためです。この問題を解決する方法の一つは、下記のプログラムの 2、3 行目ように、__`ax` と `ai`__ にそれぞれ __`self.ax` と `self.ai` を代入する処理を記述__ するというものです。このように修正することで、削除した __仮引数 `ax` と `ai` と同じ名前のローカル変数にそれぞれの値が代入__ されるので、以後のプログラムを変更する必要が無くなります。

```python
1  def draw_board(self):
2      ax = self.ax
3      ai = self.ai
4    
5     # Axes の内容をクリアして、これまでの描画内容を削除する
6      ax.clear()
元と同じなので省略
7    
8  Marubatsu_GUI.draw_board = draw_board
```

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

```python
def draw_board(self):
    ax = self.ax
    ai = self.ai
    
    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()
    
    # y 軸を反転させる
    ax.invert_yaxis()
    
    # 枠と目盛りを表示しないようにする
    ax.axis("off")   
    
    # ゲームの決着がついていた場合は背景色を
    facecolor = "white" if self.mb.status == Marubatsu.PLAYING else "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
    ax.text(0, -0.2, text, fontsize=20)
    
    # ゲーム盤の枠を描画する
    for i in range(1, self.mb.BOARD_SIZE):
        ax.plot([0, self.mb.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
        ax.plot([i, i], [0, self.mb.BOARD_SIZE], c="k") # 縦方向の枠線   

    # ゲーム盤のマークを描画する
    for y in range(self.mb.BOARD_SIZE):
        for x in range(self.mb.BOARD_SIZE):
            color = "red" if (x, y) == self.mb.last_move else "black"
            self.mb.draw_mark(ax, x, y, self.mb.board[x][y], color)            

    self.update_widgets_status()   
    
Marubatsu_GUI.draw_board = draw_board
```
</details>

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

```diff_python
-def draw_board(self, ax, ai):
+def draw_board(self):
+   ax = self.ax
+   ai = self.ai
    
    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()
元と同じなので省略
    
Marubatsu_GUI.draw_board = draw_board
```
</details>

:::note
先程のノートで説明したように、上記の修正を行った後で、シンボル名の変更を使って `ax` と `ai` を `self.ax` と `self.ai` に変更し、その後で、2、3 行目の代入文を削除するという方法で、`ax` と `ai` を `self.ax` と `self.ai` に変更することができます。

`draw_board` の処理の中で、`ai` と `ax` に何らかの値を代入して変更する処理を行う必要がある場合は、`ax` と `self.ax` の中身が別々のものになるため `ax` と `ai` を `self.ax` と `self.ai` に変更する必要がありますが、実際にはそのような処理は行われないので、わざわざそのような作業を行うメリットはあまりないでしょう。
:::

実行結果は省略しますが、上記の修正後に、下記のプログラムを実行して正しい処理が行われることを確認して下さい。

* __2 行目__：`draw_board` の実引数の記述を削除する

```python
mb_gui = Marubatsu_GUI(mb, ai=[None, None])
mb_gui.draw_board()
```

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

```diff_python
mb_gui = Marubatsu_GUI(mb, ai=[None, None])
-mb_gui.draw_board(mb_gui.ax, mb_gui.ai)
+mb_gui.draw_board()
```
</details>

以上が、`Marubatsu_GUI` クラスでの `draw_board` メソッドの定義です。`self` に関するバグは初心者にはわかりづらいかもしれませんが、よくあるバグの原因の一つです。

:::note warn
`self` を記述する場合は、__`self` が具体的に何を意味しているかを意識しながらプログラムを記述することを心掛けて下さい__。
:::

# `play` メソッドの修正

まだイベントハンドラに関する処理を記述していませんが、GUI に関する描画をできるようになったので、__`play` メソッドの中で `Marubatsu_GUI` を利用する__ ように修正します。

下記は、そのように `play` メソッドを修正したプログラムです。GUI に関する処理は、`Marubatsu_GUI` の中ですべて行うことにしたので __非常にシンプルなプログラムになります__。

```python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)

    # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
    if gui:
        Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)    
        
    self.restart()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play
```

上記の修正後に、下記のプログラムで、`gui_play` を実行すると、ボタンとゲーム盤を描画するための Figure が表示されますが、実行結果のようなエラーが発生します。このエラーの原因について少し考えてみて下さい。

```python
from util import gui_play

gui_play()
```

実行結果（下図は、画像なので操作することはできません）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/42486a3f-59af-fae2-8fd8-42779df798ee.png" width="300">

```
略
Cell In[14], line 11
      8     Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)    
     10 self.restart()
---> 11 return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

NameError: name 'ax' is not defined
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/13321022-502a-c14f-ff98-26c484d73b1e.png" width="200">

## エラーの検証と修正

エラーメッセージから、__`play_loop`__ を呼び出す際に __実引数に記述した `ax` が定義されていない__ ことがわかります。__`ax`__ は、__`Marubatsu_GUI`__ クラスのインスタンスの __属性に代入__ されているので、下記のプログラムの 3 行目のように、__`Marubatsu_GUI` のインスタンスを `mb_gui`__ に代入し、__`play_loop` の実引数__ にキーワード引数 __`ax=mb_gui.ax` を記述__ することでこの問題を解決することができます。

```python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):   
元と同じなので省略
    return self.play_loop(ai=ai, ax=mb_gui.ax, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play
```

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

```python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)

    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        mb_gui = Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)    
        
    self.restart()
    return self.play_loop(ai=ai, ax=mb_gui.ax, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play
```
</details>

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

```diff_python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):   
元と同じなので省略
-   return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
+   return self.play_loop(ai=ai, ax=mb_gui.ax, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play
```
</details>

上記の修正後に、下記のプログラムで `gui_play` を実行すると、実行結果のような別のエラーが発生します。このエラーの原因について少し考えてみて下さい。

```python
gui_play()
```

実行結果

```
略
File c:\Users\ys\ai\marubatsu\082\marubatsu.py:682, in Marubatsu.draw_board.<locals>.update_widgets_status()
    680 def update_widgets_status():
    681     # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
--> 682     set_button_status(self.first_button, self.move_count <= 0)
    683     set_button_status(self.prev_button, self.move_count <= 0)
    684     set_button_status(self.next_button, self.move_count >= len(self.board_records) - 1)

AttributeError: 'Marubatsu' object has no attribute 'first_button'
```

## 新たなエラーの検証と修正

エラーメッセージから、__`update_widgets_status` 内でエラーが発生__ していることがわかりますが、よく見るとエラーが発生している下記の行の内容が、先程定義した __`Marubatsu_GUI` のメソッドとは異なる__ ことがわかります。

```python
# エラーメッセージ内のエラーが発生している行
set_button_status(self.first_button, self.move_count <= 0)

# Marubatsu_GUI クラスの update_widgets_status の該当する行の記述
set_button_status(self.first_button, self.mb.move_count <= 0)
```

また、エラーメッセージの __`in Marubatsu.draw_board.<locals>.update_widgets_status()`__ から、エラーが発生した `update_widgets_status` が __`Marubatsu` クラスの `draw_board` 内で定義されたローカル関数__ であることがわかります。従って、このエラーは、__`play_loop` の中__ で、古い __`Marubatsu`__ クラスの __`draw_board` を呼び出している__ ことが原因なので、新しい __`Marubatsu_GUI`__ の __`draw_board` メソッドを呼び出す__ ように修正することで解決できます。

`play_loop` 内で `draw_board` は下記のプログラムのように記述して呼び出されています。

```python
self.draw_board(ax, ai)
```

この __`self`__ は __`Marubatsu` クラスのインスタンス__ なので、__`Marubatsu_GUI` の `draw_board`__ を呼び出すように修正するためには、__`Marubatsu_GUI` クラスのインスタンスの情報が必要__ になりますが、その情報は現状では `play_loop` メソッド内では利用できません。そこで、下記のプログラムの 4 行目のように、__`play` メソッド内で、`Marubatsu_GUI` のインスタンスを作成した際__ に、__`Marubatsu`__ クラスの __`mb_gui` という属性に代入__ することにします。

* __4、7 行目__：`mb_gui` を `self.mb_gui` に修正する

```python
1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):   
元と同じなので省略
2      # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
3      if gui:
4          self.mb_gui = Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)    
5        
6      self.restart()
7      return self.play_loop(ai=ai, ax=self.mb_gui.ax, params=params, verbose=verbose, gui=gui)
8
9  Marubatsu.play = play
```

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

```python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)

    # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
    if gui:
        self.mb_gui = Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)    
        
    self.restart()
    return self.play_loop(ai=ai, ax=self.mb_gui.ax, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play
```
</details>

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

```diff_python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):   
元と同じなので省略
    # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
    if gui:
-       Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)    
+       self.mb_gui = Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)    
        
    self.restart()
-   return self.play_loop(ai=ai, ax=mb_gui.ax, params=params, verbose=verbose, gui=gui)
+   return self.play_loop(ai=ai, ax=self.mb_gui.ax, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play
```
</details>

上記のように修正することで、下記のプログラムのように、`play_loop` 内で `Marubatsu_GUI` クラスの `draw_board` メソッドを利用できるようになります。

* __4、6 行目__：`Marubatsu_GUI` クラスの `draw_board` メソッドを呼び出すように修正する

```python
1  def play_loop(self, ai, ax, params, verbose, gui):
元と同じなので省略
2                  # AI どうしの対戦の場合は画面を描画しない
3                  if ai[0] is None or ai[1] is None:
4                      self.mb_gui.draw_board()
元と同じなので省略
5          if gui:
6              self.mb_gui.draw_board()
元と同じなので省略
7
8  Marubatsu.play_loop = play_loop
```

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

```python
def play_loop(self, ai, ax, params, verbose, gui):
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ゲーム盤の表示
        if verbose:
            if gui:
                # AI どうしの対戦の場合は画面を描画しない
                if ai[0] is None or ai[1] is None:
                    self.mb_gui.draw_board()
                # 手番を人間が担当する場合は、play メソッドを終了する
                if ai[index] is None:
                    return
            else:
                print(self)
                
        # ai が着手を行うかどうかを判定する
        if ai[index] is not None:
            x, y = ai[index](self, **params[index])
        else:
            # キーボードからの座標の入力
            coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
            # "exit" が入力されていればメッセージを表示して関数を終了する
            if coord == "exit":
                print("ゲームを終了します")
                return       
            # x 座標と y 座標を要素として持つ list を計算する
            xylist = coord.split(",")
            # xylist の要素の数が 2 ではない場合
            if len(xylist) != 2:
                # エラーメッセージを表示する
                print("x, y の形式ではありません")
                # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
                continue
            x, y = xylist
        # (x, y) に着手を行う
        try:
            self.move(int(x), int(y))
        except:
            print("整数の座標を入力して下さい")

    # 決着がついたので、ゲーム盤を表示する
    if verbose:
        if gui:
            self.mb_gui.draw_board()
        else:
            print(self)
            
    return self.status

Marubatsu.play_loop = play_loop
```
</details>

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

```diff_python
def play_loop(self, ai, ax, params, verbose, gui):
元と同じなので省略
                # AI どうしの対戦の場合は画面を描画しない
                if ai[0] is None or ai[1] is None:
-                   self.draw_board(ax, ai)
+                   self.mb_gui.draw_board()
元と同じなので省略
        if gui:
-           self.draw_board(ax, ai)
+           self.mb_gui.draw_board()
元と同じなので省略

Marubatsu.play_loop = play_loop
```
</details>

実行結果は省略しますが、上記の修正後に、下記のプログラムで `gui_play` を実行して正しい処理が行われることを確認して下さい。

```python
gui_play()
```

## 必要のない仮引数の削除

VSCode で `play_loop` の定義を見ると下図のように __仮引数 `ax` が薄い色で表示__ されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3c2509a8-401a-4718-df6d-8567c3b2ff11.png" width="350">

VSCode では、「__定義されているが、その後一度も利用されない名前__」がこのように __薄い色で表示__ されます。そのような名前はエラーではありませんが、その名前はその後で使われていないので、__必要がない可能性が高い__ ことを意味します。実際に __`ax`__ は、先程の修正で `self.draw_board(ax, ai)` を `self.mb_gui.draw_board()` に修正した結果、__`draw_board` のブロックの内で一度も利用されなくなる__ ので、__削除することができます__。そこで下記のプログラムの 1 行目のように、`play_loop` から 仮引数 `ax` を削除することにします。

```python
def play_loop(self, ai, params, verbose, gui):
元と同じなので省略

Marubatsu.play_loop = play_loop
```

<details><summary><strong>プログラム全体</strong></summary>

```python
def play_loop(self, ai, params, verbose, gui):
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ゲーム盤の表示
        if verbose:
            if gui:
                # AI どうしの対戦の場合は画面を描画しない
                if ai[0] is None or ai[1] is None:
                    self.mb_gui.draw_board()
                # 手番を人間が担当する場合は、play メソッドを終了する
                if ai[index] is None:
                    return
            else:
                print(self)
                
        # ai が着手を行うかどうかを判定する
        if ai[index] is not None:
            x, y = ai[index](self, **params[index])
        else:
            # キーボードからの座標の入力
            coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
            # "exit" が入力されていればメッセージを表示して関数を終了する
            if coord == "exit":
                print("ゲームを終了します")
                return       
            # x 座標と y 座標を要素として持つ list を計算する
            xylist = coord.split(",")
            # xylist の要素の数が 2 ではない場合
            if len(xylist) != 2:
                # エラーメッセージを表示する
                print("x, y の形式ではありません")
                # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
                continue
            x, y = xylist
        # (x, y) に着手を行う
        try:
            self.move(int(x), int(y))
        except:
            print("整数の座標を入力して下さい")

    # 決着がついたので、ゲーム盤を表示する
    if verbose:
        if gui:
            self.mb_gui.draw_board()
        else:
            print(self)
            
    return self.status

Marubatsu.play_loop = play_loop
```
</details>

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

```diff_python
-def play_loop(self, ai, ax, params, verbose, gui):
+def play_loop(self, ai, params, verbose, gui):
元と同じなので省略

Marubatsu.play_loop = play_loop
```
</details>

`play_loop` の仮引数を修正したので、`play` メソッド内で `play_loop` を呼び出す処理を下記のプログラムの 3 行目のように修正する必要があります。

```python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):   
元と同じなので省略
    return self.play_loop(ai=ai, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play
```

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

```python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)

    # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
    if gui:
        self.mb_gui = Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)    
        
    self.restart()
    return self.play_loop(ai=ai, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play
```
</details>

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

```diff_python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):   
元と同じなので省略
-   return self.play_loop(ai=ai, ax=self.mb_gui.ax, params=params, verbose=verbose, gui=gui)
+   return self.play_loop(ai=ai, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play
```
</details>


実行結果は省略しますが、上記の修正後に、下記のプログラムで `gui_play` を実行して正しい処理が行われることを確認して下さい。

```python
gui_play()
```

## 他の `play_loop` の仮引数の削除

以下のように思った人はいないでしょうか？

* `ai` は `Marubatsu_GUI` の属性に代入されるので、`play_loop` の仮引数 `ai` がなくても `play_loop` のブロックの中では `self.mb_gui.ai` と記述することで参照できる
* 同様の理由で `play_loop` の仮引数 `params` がなくても `self.mb_gui.params` と記述することで参照できる 
* 従って、`ax` と同様に、`ai` と `params` を `play_loop` の仮引数から削除できる

しかし、残念ながら下記の理由から、`ai` や `params` を `play_loop` の仮引数から削除することはできません。

* __`Marubatsu_GUI` のインスタンス__ は、`play` メソッドの中で、__GUI で〇×ゲームを遊ぶ場合でしか作られない__
* __CUI でゲームを遊ぶ場合__ でも `play_loop` は呼び出されるが、__その場合は `self.mb_gui` には何も代入されていない__ ので利用できない

しかし、別の方法で `play_loop` の仮引数を削除することができます。現状では、`play_loop` には `ai`、`params`、`verbose`、`gui` の 4 つの仮引数がありますが、これらを `Marubatsu_GUI` ではなく、__`Marubatsu` クラスのインスタンスの属性に代入__ することで、__`play_loop` の仮引数を削除__ することができます。そこで、そのように `play` メソッドを修正することにします。

* __3 ~ 6 行目__：`play_loop` の実引数に記述するデータを `Marubatsu` クラスのインスタンスの属性に代入する
* __7 行目__：`play_loop` の実引数を削除する

```python
1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
2      # 一部の仮引数をインスタンスの属性に代入する
3      self.ai = ai
4      self.params = params
5      self.verbose = verbose
6      self.gui = gui
元と同じなので省略    
7      return self.play_loop()
8
9  Marubatsu.play = play
```

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

```python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
    # 一部の仮引数をインスタンスの属性に代入する
    self.ai = ai
    self.params = params
    self.verbose = verbose
    self.gui = gui
    
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)

    # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
    if gui:
        self.mb_gui = Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)    
        
    self.restart()
    return self.play_loop()

Marubatsu.play = play
```
</details>

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

```diff_python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
    # 一部の仮引数をインスタンスの属性に代入する
+   self.ai = ai
+   self.params = params
+   self.verbose = verbose
+   self.gui = gui
元と同じなので省略    
-   return self.play_loop(ai=ai, params=params, verbose=verbose, gui=gui)
+   return self.play_loop()

Marubatsu.play = play
```
</details>

次に、`play_loop` を下記のプログラムのように修正します。

* __1 行目__：`self` 以外の仮引数を削除する
* __2 ~ 5 行目__：削除した仮引数と同じ名前のローカル変数に、対応する属性を代入する

```python
1  def play_loop(self):
2      ai = self.ai
3      params = self.params
4      verbose = self.verbose
5      gui = self.gui
元と同じなので省略
6
7  Marubatsu.play_loop = play_loop
```

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

```python
def play_loop(self):
    ai = self.ai
    params = self.params
    verbose = self.verbose
    gui = self.gui
    
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ゲーム盤の表示
        if verbose:
            if gui:
                # AI どうしの対戦の場合は画面を描画しない
                if ai[0] is None or ai[1] is None:
                    self.mb_gui.draw_board()
                # 手番を人間が担当する場合は、play メソッドを終了する
                if ai[index] is None:
                    return
            else:
                print(self)
                
        # ai が着手を行うかどうかを判定する
        if ai[index] is not None:
            x, y = ai[index](self, **params[index])
        else:
            # キーボードからの座標の入力
            coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
            # "exit" が入力されていればメッセージを表示して関数を終了する
            if coord == "exit":
                print("ゲームを終了します")
                return       
            # x 座標と y 座標を要素として持つ list を計算する
            xylist = coord.split(",")
            # xylist の要素の数が 2 ではない場合
            if len(xylist) != 2:
                # エラーメッセージを表示する
                print("x, y の形式ではありません")
                # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
                continue
            x, y = xylist
        # (x, y) に着手を行う
        try:
            self.move(int(x), int(y))
        except:
            print("整数の座標を入力して下さい")

    # 決着がついたので、ゲーム盤を表示する
    if verbose:
        if gui:
            self.mb_gui.draw_board()
        else:
            print(self)
            
    return self.status

Marubatsu.play_loop = play_loop
```
</details>

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

```diff_python
-def play_loop(self, ai, params, verbose, gui):
+def play_loop(self):
+   ai = self.ai
+   params = self.params
+   verbose = self.verbose
+   gui = self.gui
元と同じなので省略

Marubatsu.play_loop = play_loop
```
</details>

:::note
シンボル名の変更を使って `ai` などを `ai.self` に変更することは可能ですが、面倒なだけで大きなメリットはないので本記事では採用しないことにします。
:::

## 重複する属性の削除

上記で、`Marubatsu` クラスのインスタンスに `ai`、`params`、`verbose`、`gui` の 4 つの属性を追加しましたが、上記の中の `ai` と `params` の値は、`Marubatsu_GUI` クラスの `__init__` メソッドの中で、下記のプログラムのように `Marubatsu_GUI` クラスのインスタンスの属性にも代入しています。

```python
def __init__(self, mb, ai, ai_dict=None, params=None, size=3):
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]

    self.mb = mb
    self.ai = ai
    self.ai_dict = ai_dict
    self.ai_params = params
    self.size = size
以下略
```

__同じデータ__ を `Marubatsu` と `Marubatsu_GUI` の両方のインスタンスに __重複して代入するのは無駄__ なので、__`ai`__ と __`params`__ は __`Marubatsu`__ クラスの __インスタンスの属性のみに代入する__ ことにします。まず、`Marubatsu_GUI` クラスの `__init__` メソッドを下記のプログラムのように修正します。なお、__`ai_dict`__ と __`size`__ は、__GUI で〇×ゲームを遊ぶ際でのみ必要となる__ データなので、__これまで通り `Marubatsu_GUI` の属性に代入する__ ことにします。

* __1 行目__：仮引数 `ai` と `params` を削除する
* `ai` と `params` に関する処理を削除する

```python
 1  def __init__(self, mb, ai_dict=None, size=3):
 2      # ai_dict が None の場合は、空の list で置き換える
 3      if ai_dict is None:
 4          ai_dict = {}
 5
 6      self.mb = mb
 7      self.ai_dict = ai_dict
 8      self.size = size
元と同じなので省略
 9
10  Marubatsu_GUI.__init__ = __init__
```

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

```python
def __init__(self, mb, ai_dict=None, size=3):
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}

    self.mb = mb
    self.ai_dict = ai_dict
    self.size = size
    
    # %matplotlib widget のマジックコマンドを実行する
    get_ipython().run_line_magic('matplotlib', 'widget')
        
    self.create_widgets()
    self.display_widgets() 
        
Marubatsu_GUI.__init__ = __init__
```
</details>

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

```diff_python
-def __init__(self, mb, ai, ai_dict=None, params=None, size=3):
+def __init__(self, mb, ai_dict=None, size=3):
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}
    # params が None の場合のデフォルト値を設定する
-   if params is None:
-       params = [{}, {}]

    self.mb = mb
-   self.ai = ai
    self.ai_dict = ai_dict
-   self.ai_params = params
    self.size = size
元と同じなので省略

Marubatsu_GUI.__init__ = __init__
```
</details>

次に、`play` メソッドを下記のプログラムのように修正します。

* __3、4 行目__：`__init__` メソッド内に記述されていた、`params` が `None` の場合の処理を記述する[^2]
* __10 行目__：`Marubatsu_GUI` の実引数から `ai` と `params` を削除する

```python
 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
 2      # params が None の場合のデフォルト値を設定する
 3      if params is None:
 4          params = [{}, {}]
 5       
 6      # 一部の仮引数をインスタンスの属性に代入する
 7      self.ai = ai
元と同じなので省略
 8      # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
 9      if gui:
10          self.mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, size=size)    
元と同じなので省略
11
12  Marubatsu.play = play
```

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

```python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
        
    # 一部の仮引数をインスタンスの属性に代入する
    self.ai = ai
    self.params = params
    self.verbose = verbose
    self.gui = gui
    
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)

    # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
    if gui:
        self.mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, size=size)    
        
    self.restart()
    return self.play_loop()

Marubatsu.play = play
```
</details>

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

```diff_python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
    # params が None の場合のデフォルト値を設定する
+   if params is None:
+       params = [{}, {}]
        
    # 一部の仮引数をインスタンスの属性に代入する
    self.ai = ai
元と同じなので省略
    # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
    if gui:
-       self.mb_gui = Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)     
+       self.mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, size=size)    
元と同じなので省略

Marubatsu.play = play
```
</details>

上記の修正後に、下記のプログラムで、`gui_play` を実行すると、実行結果のようなエラーが発生します。エラーの原因について少し考えてみて下さい。

```python
gui_play()
```

実行結果

```
略
File c:\Users\ys\ai\marubatsu\082\marubatsu.py:770, in Marubatsu_GUI.create_dropdown(self)
    767 # ai に代入されている内容を ai_dict に追加する
    768 for i in range(2):
    769     # ラベルと項目の値を計算する
--> 770     if self.ai[i] is None:
    771         label = "人間"
    772         value = "人間"

AttributeError: 'Marubatsu_GUI' object has no attribute 'ai'
```

### エラーの検証と修正

エラーメッセージから、`create_dropdown` 内の `self.ai[i]` で、`Marubatsu_GUI` クラスのインスタンスに `ai` が存在しないことが原因であることが分かります。これは、先程 __`ai` のデータを `Marubatsu_GUI` クラスの属性に代入しないようにした__ ことが原因です。

従って、このエラーは、`Marubatsu_GUI` の中で `ai` 属性を利用する処理を、`self.ai` から `self.mb.ai` に修正することで解決できます。

まず、`create_dropwodn` メソッドを下記のプログラムのように修正します。

* __7、11、12 行目__：`self.ai` を `self.mb.ai` に修正します

```python
 1  import ipywidgets as widgets
 2
 3  def create_dropdown(self):
元と同じなので省略
 4      # ai に代入されている内容を ai_dict に追加する
 5      for i in range(2):
 6          # ラベルと項目の値を計算する
 7          if self.mb.ai[i] is None:
 8              label = "人間"
 9              value = "人間"
10          else:
11              label = self.mb.ai[i].__name__        
12              value = self.mb.ai[i]
元と同じなので省略
13
14  Marubatsu_GUI.create_dropdown = create_dropdown
```

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

```python
import ipywidgets as widgets

def create_dropdown(self):
    # それぞれの手番の担当を表す 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],
            )
        )      
        
Marubatsu_GUI.create_dropdown = create_dropdown
```
</details>

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

```diff_python
import ipywidgets as widgets

def create_dropdown(self):
元と同じなので省略
    # ai に代入されている内容を ai_dict に追加する
    for i in range(2):
        # ラベルと項目の値を計算する
-       if self.ai[i] is None:
+       if self.mb.ai[i] is None:
            label = "人間"
            value = "人間"
        else:
-           label = self.ai[i].__name__        
+           label = self.mb.ai[i].__name__        
-           value = self.ai[i]
+           value = self.mb.ai[i]
元と同じなので省略

Marubatsu_GUI.create_dropdown = create_dropdown
```
</details>

`ai` は `draw_board` メソッドでも利用されているので、`draw_board` メソッドを下記のプログラムの 3 行目のように修正する必要があります。

```python
def draw_board(self):
    ax = self.ax
    ai = self.mb.ai
元と同じなので省略    
    
Marubatsu_GUI.draw_board = draw_board
```

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

```python
def draw_board(self):
    ax = self.ax
    ai = self.mb.ai
    
    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()
    
    # y 軸を反転させる
    ax.invert_yaxis()
    
    # 枠と目盛りを表示しないようにする
    ax.axis("off")   
    
    # ゲームの決着がついていた場合は背景色を
    facecolor = "white" if self.mb.status == Marubatsu.PLAYING else "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
    ax.text(0, -0.2, text, fontsize=20)
    
    # ゲーム盤の枠を描画する
    for i in range(1, self.mb.BOARD_SIZE):
        ax.plot([0, self.mb.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
        ax.plot([i, i], [0, self.mb.BOARD_SIZE], c="k") # 縦方向の枠線   

    # ゲーム盤のマークを描画する
    for y in range(self.mb.BOARD_SIZE):
        for x in range(self.mb.BOARD_SIZE):
            color = "red" if (x, y) == self.mb.last_move else "black"
            self.mb.draw_mark(ax, x, y, self.mb.board[x][y], color)            

    self.update_widgets_status()   
    
Marubatsu_GUI.draw_board = draw_board
```
</details>

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

```diff_python
def draw_board(self):
    ax = self.ax
-   ai = self.ai
+   ai = self.mb.ai
元と同じなので省略    
    
Marubatsu_GUI.draw_board = draw_board
```
</details>

実行結果は省略しますが、上記の修正後に、下記のプログラムで `gui_play` を実行して正しい処理が行われることを確認して下さい。

```python
gui_play()
```

# 今回の記事のまとめ

今回の記事では、__`Marubatsu_GUI` クラスを定義__ することで、__`Marubatsu` クラスから GUI の機能を分離__ する作業の中で、ゲーム盤の描画を行う処理を実装しました。

その際に、同じ __`self` が異なる意味で使われることに由来する多くのエラー__ を紹介し、エラーの修正を行いました。

`self` に限らず、__名前の混同に関するエラー__ はよくあるエラーなので原因と対処方法についてしっかりと理解しておくことを強くお勧めします。

なお、現状ではまだイベントハンドラの機能を `Marubatsu_GUI` に記述していないので、ゲームを遊ぶことはできません。また、現状のプログラムにはまだいくつかの問題があります。イベントハンドラの機能の実装と問題の修正は次回の記事で行います。

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

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

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

以下のリンクは、今回の記事で更新した marubatsu.py です。なお、`Marubatsu` クラスの `draw_board` はもう必要がなくなったので削除しました。

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

# 次回の記事

https://qiita.com/ysgeso/items/e857737e7f36a3a242bc

[^1]:シンボル名とは、変数や関数などの名前の事を表します
[^2]:実は、前回の記事でこの処理を `__init__` メソッドに移動したのは間違いでした。その理由は、`params` に `None` が代入されていた場合は、`Marubatsu_GUI` の `params` 属性には `[{}, {}]` が代入されますが、`play` メソッドのローカル変数の中身は `None` のままになってしまうためです