Pythonで〇×ゲームのAIを一から作成する　その76　playメソッドのバグの修正とデフォルト引数に関する注意点

# 目次と前回の記事

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

https://qiita.com/ysgeso/items/64de676735836b963712

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

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

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

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

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

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

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

# `play` メソッドのバグ

前回の記事で、__`play`__ メソッドを __実行__ した際に __仮引数 `ai`__ に __代入__ されている __値を元__ に、__Dropdown の項目__ を __自動的に設定__ するプログラムを __記述__ しましたが、その際に __`play`__ メソッドには __重大なバグ__ が __いくつか存在__ するという説明を行いました。

## バグその 1（人間の項目が複数作られる）

__下記__ は、__`play`__ メソッドで __人間__ VS __AI__ の __対戦__ を行うプログラムです。__実行結果__ の __下図左__ から、__正しく動作__ しているように __見えるかも__ しれませんが、__Dropdown の上__ で __マウスを押す__ と、__右下図__ のように、__人間の項目__ が __複数表示__ されるという __問題が発生__ することが __確認__ できます。このようなことが起きる原因について少し考えてみて下さい。

```python
%matplotlib widget
from marubatsu import Marubatsu

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

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/93e865f0-d80e-9ae5-b939-35c25d552daf.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/b6f3438c-d794-23ba-b3f0-c0f66d7d42dd.png" width="200">

### バグの原因の考察

下記は、__`play`__ メソッドの中で、__Dropdown の項目__ を __作成__ する部分のプログラムです。

```python
 1  # ai に代入されている内容を ai_list に追加する
 2  for i in range(2):
 3      # ai[i] が ai_values に登録済かどうかを判定する
 4      if ai[i] not in ai_values:
 5          # ラベルと項目の値を計算する
 6          if ai[i] is None:
 7              label = "人間"
 8              value = "人間"
 9          else:
10              label = ai[i].__name__        
11              value = ai[i]
12          # 項目を登録する
13          ai_list.append((label, value))
14          ai_values.append(value)
15          select_values.append(value)
```                

__下記__ は、__上記の中__ で __Dropdown の項目__ に __関する処理__ を __箇条書き__ にしたものです。

* __4 行目__：__`ai[i]`__ が __`ai_values`__ に __登録済か__ どうかを __判定__ する
* __8 行目__：__人間の担当__ の場合は、__`value`__ に __`None`__ を __代入__ する
* __11 行目__：__AI の担当__ 場合は、__`value`__ に __`ai[i]`__ を __代入__ する
* __14 行目__：__`ai_values`__ に __`value`__ を __追加__ する

__上記__ の __4 行目__ で __`ai_values`__ に __登録済であるか__ どうかを __判定__ する際に使われている __`ai[i]`__ と、__15 行目__ で __`ai_values`__ に __追加__ する、__`value` に代入__ されている __値__ が、__人間__ が __担当する場合__ は __異なる点__ に __注目__ して下さい。__人間__ の __担当の場合__ には __`ai_values`__ に __`"人間"` が登録__ されるため、__次の繰り返し処理__ が __人間の手番__ だった場合に、__`ai[i] in ai_values`__ が __`False`__ になるので __もう一度__ 15 行目で __`ai_values`__ に __`"人間"` が登録__ されてしまいます。

言葉だけの説明ではわかりづらいと思いますので、__人間__ VS __人間__ の場合で __行われる処理__ で、__関連__ する __変数の値__ が __どのように変化__ するかを __下記の表__ で __示します__。なお、__条件式__ の __列__ は、__その行__ の __条件式の値__ を表します。__表から__、__上記__ のプログラムを実行すると、__`ai_list`__ に __人間の項目__ が __2 つ登録__ されてしまうことが __わかります__。

|  | 条件式 | `i` | `ai[i]` | `value` | `ai_values` | `ai_list` |
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| __2 行目__  | |  `0`  | `None`  | |`[]` | `[]` |
| __4 行目__  | `False` | `0`  | `None` | | `[]` | `[]` |
| __6 行目__  | `False`  | `0`  | `None`|  | `[]` | `[]` |
| __8 行目__  |  | `0`  | `None`  | `"人間"` |`[]` | `[]` |
| __13 行目__  |  | `0`  | `None`  | `"人間"`| `["人間"]` | `[]` |
| __14 行目__  |  | `0`  | `None`  | `"人間"`| `["人間"]` | `[("人間", "人間)]` |
| __2 行目__  |  |  `1`  | `None`  |  |`["人間"]` | `[("人間", "人間)]` |
| __4 行目__  |  `False` | `1`  | `None` | | `["人間"]` | `[("人間", "人間)]` |
| __6 行目__  |  `False`  | `1`  | `None` | | `["人間"]` | `[("人間", "人間)]` |
| __8 行目__  |  | `1`  | `None`  |  `"人間"`| `["人間"]` |`[("人間", "人間)]` |
| __13 行目__  |    | `1`  | `None`  |  `"人間"`| `["人間", "人間"]` | `[("人間", "人間)]` |
| __14 行目__  |    | `1`  | `None`  |  `"人間"`| `["人間", "人間"]` | `[("人間", "人間),`<br>`("人間", "人間)]` |

プログラムを __確認できる__ ように、__コメントを除いた__ プログラムを __下記__ に __再掲__ します。

```python
 2  for i in range(2):
 4      if ai[i] not in ai_values:
 6          if ai[i] is None:
 7              label = "人間"
 8              value = "人間"
 9          else:
10              label = ai[i].__name__        
11              value = ai[i]
13          ai_list.append((label, value))
14          ai_values.append(value)
15          select_values.append(value)
```   

:::note
__本当__ は、__2 回目__ の __2、4、6 行目__ では、__`value`__ に __`"人間"`__ が __代入されたまま__ になっていますが、__8 行目__ で __改めて__ `value` に __`"人間"`__ が __代入される__ ことを __明確するため__ に __空欄__ に __しました__。
:::

### バグの修正

__この問題__ は、__4 行目__ の __条件式__ で、__`ai_values`__ に __実際__ に __登録する値__ を __使って__、__in 演算子__ で __判定を行う__ ことで __解決__ することが __できます__。__そのため__ には、__`ai_values`__ に __登録__ する __`value`__ を __計算__ する __6 ~ 13 行目__ にあった __処理__ を、__4 行目__ の __条件式より__ も __前に移動__ する __必要__ が __あります__。__下記__ は、__そのよう__ に __`play`__ メソッドを __修正__ したプログラムです。

* __9 ~ 14 行目__：__16 行目__ の __if 文__ の __ブロックの中__ に __記述__ されていた __処理__ を、__if 文__ の __ブロックの前__ に __移動__ する

```python
 1  import matplotlib.pyplot as plt
 2  import ipywidgets as widgets
 3  import math
 4
 5  def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 6      # ai に代入されている内容を ai_list に追加する
 7      for i in range(2):
 8          # ラベルと項目の値を計算する
 9          if ai[i] is None:
10              label = "人間"
11              value = "人間"
12          else:
13              label = ai[i].__name__        
14              value = ai[i]
15          # value が ai_values に登録済かどうかを判定する
16          if value not in ai_values:
17              # 項目を登録する
18              ai_list.append((label, value))
19              ai_values.append(value)
20              select_values.append(value)
元と同じなので省略    
21
22  Marubatsu.play = play
```

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

```python
import matplotlib.pyplot as plt
import ipywidgets as widgets
import math

def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # ai_list から、項目だけを取り出した list を作成する
    ai_values = [value for label, value in ai_list]
    # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
    select_values = []
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        # ラベルと項目の値を計算する
        if ai[i] is None:
            label = "人間"
            value = "人間"
        else:
            label = ai[i].__name__        
            value = ai[i]
        # value が ai_values に登録済かどうかを判定する
        if value not in ai_values:
            # 項目を登録する
            ai_list.append((label, value))
            ai_values.append(value)
            select_values.append(value)
    
    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_list,
        description="〇",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[0],
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_list,
        description="×",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[1],
    )       
    
    # リセットボタンを作成する
    button = widgets.Button(
        description="リセット",
        layout=widgets.Layout(width="100px"),
    )
    
    # 〇 と × の dropdown と リセットボタンを横に配置した HBox を作成し、表示する
    hbox = widgets.HBox([dropdown_circle, dropdown_cross, button])
    display(hbox)
    
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()
        self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)           
        
    # イベントハンドラをリセットボタンに結びつける
    button.on_click(on_button_clicked)

    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        fig, ax = plt.subplots(figsize=[size, size])
        fig.canvas.toolbar_visible = False
        fig.canvas.header_visible = False
        fig.canvas.footer_visible = False
        fig.canvas.resizable = False          
        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合のみ処理を行う
            if event.inaxes and self.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                self.move(x, y)                
                self.draw_board(ax)

                # 次の手番の処理を行うメソッドを呼び出す
                self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
                
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     

    self.restart()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

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

```diff_python
import matplotlib.pyplot as plt
import ipywidgets as widgets
import math

def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        # ラベルと項目の値を計算する
+       if ai[i] is None:
+           label = "人間"
+           value = "人間"
+       else:
+           label = ai[i].__name__        
+           value = ai[i]
        # value が ai_values に登録済かどうかを判定する
        if value not in ai_values:
-           if ai[i] is None:
-               label = "人間"
-               value = "人間"
-           else:
-               label = ai[i].__name__        
-               value = ai[i]
            # 項目を登録する
            ai_list.append((label, value))
            ai_values.append(value)
            select_values.append(value)
元と同じなので省略    

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

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

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

実行結果

```
略
     38 dropdown_cross = widgets.Dropdown(
     39     options=ai_list,
     40     description="×",
     41     layout=widgets.Layout(width="100px"),
     42     style={"description_width": "20px"},
---> 43     value=select_values[1],
     44 )       
略
IndexError: list index out of range
```

### エラーの検証

__エラーメッセージ__ から、__`---> 43 value=select_values[1],`__ の __行__ で、__list__ の __インデックス__ に __範囲外__（out of range）の __値が設定__ されたこと が __原因__ であることが __わかります__。

__先程__ と __同様__ に、__人間__ VS __人間__ の場合で行われる __処理__ で、__関連__ する __変数の値__ が __どのように変化__ するかを __下記__ の __表__ で __示します__。

|  | 条件式 | `i` | `ai[i]` | `value` | `ai_values` | `select_values` |
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| 1 行目  | |  `0`  | `None`  | |`[]` | `[]` |
| 2 行目  | `False` | `0`  | `None` | | `[]` | `[]` |
| 4 行目  |  | `0`  | `None`  | `"人間"` |`[]` | `[]` |
| 8 行目  | `False`  | `0`  | `None`| `"人間"` | `[]` | `[]` |
| 10 行目  |  | `0`  | `None`  | `"人間"`| `["人間"]` | `[]` |
| 11 行目  |  | `0`  | `None`  | `"人間"`| `["人間"]` | `["人間"]` |
| 1 行目  |  |  `1`  | `None`  | |`["人間"]` | `["人間"]` |
| 2 行目  |  `False` | `1`  | `None` | | `["人間"]` | `["人間"]` |
| 4 行目  |   | `1`  | `None` | `"人間"`| `["人間"]` | `["人間"]` |
| 8 行目  | `True`  | `1`  | `None` | `"人間"`| `["人間"]` | `["人間"]` |

プログラムを __確認できる__ ように、__コメントを除いた__ プログラムを __下記__ に __再掲__ します。

```python
 1  for i in range(2):
 2      if ai[i] is None:
 3          label = "人間"
 4          value = "人間"
 5      else:
 6          label = ai[i].__name__        
 7          value = ai[i]
 8      if value not in ai_values:
 9          ai_list.append((label, value))
10          ai_values.append(value)
11          select_values.append(value)
```
            
__2 回目__ の __繰り返し__ の __8 行目__ の __条件式__ が __`ai[i] not in ai_values`__ から、__`value not in ai_values`__ に __変わった__ ため __`True` になる__ ので、__上記の表__ のように、__`select_values`__ には、__`["人間"]`__ という、__要素__ が __1 つ__ しかない __list__ が __代入__ されます。__そのため__、__`select_values[1]`__ を __参照__ しようとすると、__`IndexError: list index out of range`__ という __エラーが発生__ します。

__`select_values`__ は、__〇__ と __×__ の __両方の手番__ の __項目の値__ を __持つ必要__ が __あります__。__上記__ のプログラムの __問題点__ は、__11 行目__ の __`select_values`__ に __`value`__ を __追加する処理__ が、__8 行目__ の __if 文__ によって __`value`__ が __`ai_values`__ に __登録されていない場合__ でしか __行われない点__ です。従って、__下記__ のプログラムように、__`select_values`__ に __`value`__ を __追加__ する __処理__ を __8 行目__ の __前に移動__ することで、この __問題を解決__ することが __できます__。

* __12 行目__：__14 行目__ の __if 文__ の __中__ に __記述__ されていた __処理__ を __if 文__ の __前に移動__ する

```python
 1  def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2      # ai に代入されている内容を ai_list に追加する
 3      for i in range(2):
 4          # ラベルと項目の値を計算する
 5          if ai[i] is None:
 6              label = "人間"
 7              value = "人間"
 8          else:
 9              label = ai[i].__name__        
10              value = ai[i]
11          # value を select_values に常に登録する
12          select_values.append(value)
13          # value が ai_values に登録済かどうかを判定する
14          if value not in ai_values:
15              # 項目を登録する
16              ai_list.append((label, value))
17              ai_values.append(value)
元と同じなので省略
18
19  Marubatsu.play = play
```

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

```python
def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # ai_list から、項目だけを取り出した list を作成する
    ai_values = [value for label, value in ai_list]
    # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
    select_values = []
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        # ラベルと項目の値を計算する
        if ai[i] is None:
            label = "人間"
            value = "人間"
        else:
            label = ai[i].__name__        
            value = ai[i]
        # value を select_values に常に登録する
        select_values.append(value)
        # value が ai_values に登録済かどうかを判定する
        if value not in ai_values:
            # 項目を登録する
            ai_list.append((label, value))
            ai_values.append(value)
    
    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_list,
        description="〇",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[0],
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_list,
        description="×",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[1],
    )       
    
    # リセットボタンを作成する
    button = widgets.Button(
        description="リセット",
        layout=widgets.Layout(width="100px"),
    )
    
    # 〇 と × の dropdown と リセットボタンを横に配置した HBox を作成し、表示する
    hbox = widgets.HBox([dropdown_circle, dropdown_cross, button])
    display(hbox)
    
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()
        self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)           
        
    # イベントハンドラをリセットボタンに結びつける
    button.on_click(on_button_clicked)

    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        fig, ax = plt.subplots(figsize=[size, size])
        fig.canvas.toolbar_visible = False
        fig.canvas.header_visible = False
        fig.canvas.footer_visible = False
        fig.canvas.resizable = False          
        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合のみ処理を行う
            if event.inaxes and self.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                self.move(x, y)                
                self.draw_board(ax)

                # 次の手番の処理を行うメソッドを呼び出す
                self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
                
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     

    self.restart()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

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

```diff_python
def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        # ラベルと項目の値を計算する
        if ai[i] is None:
            label = "人間"
            value = "人間"
        else:
            label = ai[i].__name__        
            value = ai[i]
        # value を select_values に常に登録する
+       select_values.append(value)
        # value が ai_values に登録済かどうかを判定する
        if value not in ai_values:
            # 項目を登録する
            ai_list.append((label, value))
            ai_values.append(value)
-　　       select_values.append(value)
元と同じなので省略

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

__上記__ の __修正後__ に、__下記__ のプログラムで __`play`__ メソッドを __実行__ すると、__実行結果__ のように __正しい処理__ が __行われる__ ことが __確認__ できます。

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

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/93e865f0-d80e-9ae5-b939-35c25d552daf.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/d811ba08-d0f3-1a02-ed94-359e415ce222.png" width="200">

## バグその 2（前に表示された項目の内容が残る）

__`play`__ メソッドには __もう一つバグ__ がありますが、そのバグは __初心者__ には __気づきづらい__ バグだと思います。__具体的__ には、上記で __人間__ VS __人間__ の __対戦__ を __行った後__ で、__下記__ のプログラムで __AI__ VS __AI__ の __対戦__ を行うと、__実行結果__ の __左下図__ のように、__一見__ すると __それぞれ__ の __Dropdown__ に __正しい AI__ が __選択__ されているので、__うまくいっている__ ように __見えるかもしれません__ が、__Dropdown の上__ で __マウスを押す__ と、__右下図__ のように、__今回__ は __対戦__ を __行っていない__、__人間の項目__ が __表示__ されるという __問題が発生__ します。

```python
from ai import ai1s, ai2s

mb.play(ai=[ai1s, ai2s], gui=True);
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/ed48ea02-42ba-84ae-d869-f63252a2dc49.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/cfaf49d4-d08b-4df1-697e-d5c4a2ea8849.png" width="200">

### バグの原因の検証

この __バグの原因__ を __Python__ の __初心者が気づく__ のは __難しい__ かもしれません。実は __筆者__ も __上記__ のプログラムが __正しい__ と最初は __勘違い__ していました。この __バグの原因__ は、__下記__ のプログラムの __2 行目__ のように、__`play`__ メソッドの __最初__ に、__仮引数 `ai_list`__ の __値を表示__ することで __確認__ することができます。

```python
def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
    print("ai_list =", ai_list)
元と同じなので省略

Marubatsu.play = play
```

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

```python
def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
    print("ai_list =", ai_list)
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # ai_list から、項目だけを取り出した list を作成する
    ai_values = [value for label, value in ai_list]
    # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
    select_values = []
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        # ラベルと項目の値を計算する
        if ai[i] is None:
            label = "人間"
            value = "人間"
        else:
            label = ai[i].__name__        
            value = ai[i]
        # value を select_values に常に登録する
        select_values.append(value)
        # value が ai_values に登録済かどうかを判定する
        if value not in ai_values:
            # 項目を登録する
            ai_list.append((label, value))
            ai_values.append(value)
    
    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_list,
        description="〇",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[0],
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_list,
        description="×",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[1],
    )       
    
    # リセットボタンを作成する
    button = widgets.Button(
        description="リセット",
        layout=widgets.Layout(width="100px"),
    )
    
    # 〇 と × の dropdown と リセットボタンを横に配置した HBox を作成し、表示する
    hbox = widgets.HBox([dropdown_circle, dropdown_cross, button])
    display(hbox)
    
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()
        self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)           
        
    # イベントハンドラをリセットボタンに結びつける
    button.on_click(on_button_clicked)

    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        fig, ax = plt.subplots(figsize=[size, size])
        fig.canvas.toolbar_visible = False
        fig.canvas.header_visible = False
        fig.canvas.footer_visible = False
        fig.canvas.resizable = False          
        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合のみ処理を行う
            if event.inaxes and self.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                self.move(x, y)                
                self.draw_board(ax)

                # 次の手番の処理を行うメソッドを呼び出す
                self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
                
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     

    self.restart()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

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

```diff_python
def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
+   print("ai_list =", ai_list)
元と同じなので省略

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

__上記の修正__ を __行った後__ で、__先程__ と __同じ順番__ で、__人間__ VS __人間__ と __AI__ VS __AI__ の __対戦__ を行います。__下記__ は __人間__ VS __人間__ の __対戦__ を __行った場合__ です。なお、__実行結果__ にはこの __バグの検証__ に __必要__ な __`print(ai_list)`__ の __表示のみ__ を __表記__ します。

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

実行結果

```
ai_list = []
```

__上記__ のプログラムは、__仮引数 `ai_list`__ に __対応__ する __実引数__ を __記述していない__ ので、__`ai_list`__ には __デフォルト値__ である __`[]`__ が __代入__ され、__実行結果__ に __表示__ されます。

__続けて__ 下記のプログラムで、__AI__ VS __AI__ の __対戦__ を行います。


```python
mb.play(ai=[ai1s, ai2s], gui=True);
```

実行結果

```
ai_list = [('人間', '人間')]
```

__実行結果__ から、__先程と同様__ に __仮引数 `ai_list`__ に __対応する実引数__ を __記述していない__ にも __かかわらず__、__`ai_list`__ に __`[('人間', '人間')]`__ が __代入されている__ ことが __確認__ できます。__これが__、__Dropdown__ に __人間の項目__ が __表示__ されてしまう __原因__ です。

__この現象__ の __原因__ については、本記事で __デフォルト引数__ の __説明__ を __最初に行った__ [以前の記事](https://qiita.com/ysgeso/items/c5dbe62b329f7936181a#%E3%83%87%E3%83%95%E3%82%A9%E3%83%AB%E3%83%88%E5%BC%95%E6%95%B0%E3%81%AB%E9%96%A2%E3%81%99%E3%82%8B%E6%B3%A8%E6%84%8F%E7%82%B9)で __説明__ しましたが、__〇×ゲーム__ を __実装する際__ に __そのような例__ がこれまで __出てこなかったの__ で、ほとんどの方は忘れてしまったのではないかと思いますので、もう一度説明します。

__デフォルト引数__ の __デフォルト値__ を表す __オブジェクト__ は、__関数の定義__ が __実行__ された __際__ に __1 回だけ作成__ され、__デフォルト引数__ に __対応__ する __実引数__ が __記述されなかった__ 場合は、__そのオブジェクト__ が __デフォルト引数__ に __代入__ されます。__関数呼び出し__ が __行われた際__ に、__毎回__ デフォルト値を表す __オブジェクト__ が __新しく作成__ される __わけではない__ 点に __注意__ して下さい。

従って、__デフォルト値__ に __list など__ の __ミュータブル__ な __オブジェクト__ を __設定__ し、__関数__ の __ブロックの中__ で、__デフォルト値__ の __要素__ の __値を変更__ したり、__要素を追加__ するなどの __処理__ を行うと、__デフォルト値__ の __内容__ が __変化__ します。

__実際__ に __`play`__ メソッドでは、__`ai_list.append((label, value))`__ という、__`ai_list`__ に __デフォルト値__ が __代入__ されていた場合に、その __要素を追加__ するという __処理__ を __行っています__。そのため、__`ai_list`__ に __対応__ する __実引数__ を __記述せず__ に __人間__  VS __人間__ の __対戦を行う__ と、__デフォルト値__ が __空の list__ から、__`[("人間", "人間")]`__ に __変化__ してしまいます。そして、__その値__ が、__次__ に __AI__ VS __AI__ の __対戦__ を __`play`__ メソッドで __行った際__ に __下記__ のように __表示__ されます。

```
ai_list = [('人間', '人間')]
```

__この問題__ が __発生しない__ ようにするためには、__下記__ のプログラムのように、__list など__ の __ミュータブルな値__ を __デフォルト値__ に __したい場合__ は、__デフォルト値__ に __`None` を設定__ し、__関数__ の __ブロックの中__ で、__デフォルト引数の値__ が __`None`__ の場合に、__デフォルト引数__ に __実際__ に __設定したい値__ を __代入__ するという __処理__ を __記述する必要__ が __あります__。__このように記述__ することで、__デフォルト引数__ に __対応__ する __実引数__ を __記述しなかった__ 場合に、__デフォルト引数__ に __新しい値__ が __毎回代入__ されるので __上記__ のような __問題__ は __発生しなくなります__。

* __1 行目__：__`ai_list`__ の __デフォルト値__ を __`None`__ に __修正__ する
* __3、4 行目__：__`ai_list`__ が __`None` の場合__ に、__`ai_list`__ に __空の list__ を __代入__ する
```python
 1  def play(self, ai, ai_list=None, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
 2      # ai_list が None の場合は、空の list で置き換える
 3      if ai_list is None:
 4          ai_list = []
元と同じなので省略
 5
 6  Marubatsu.play = play
```

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

```python
def play(self, ai, ai_list=None, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
    # ai_list が None の場合は、空の list で置き換える
    if ai_list is None:
        ai_list = []
   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # ai_list から、項目だけを取り出した list を作成する
    ai_values = [value for label, value in ai_list]
    # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
    select_values = []
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        # ラベルと項目の値を計算する
        if ai[i] is None:
            label = "人間"
            value = "人間"
        else:
            label = ai[i].__name__        
            value = ai[i]
        # value を select_values に常に登録する
        select_values.append(value)
        # value が ai_values に登録済かどうかを判定する
        if value not in ai_values:
            # 項目を登録する
            ai_list.append((label, value))
            ai_values.append(value)
    
    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_list,
        description="〇",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[0],
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_list,
        description="×",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[1],
    )       
    
    # リセットボタンを作成する
    button = widgets.Button(
        description="リセット",
        layout=widgets.Layout(width="100px"),
    )
    
    # 〇 と × の dropdown と リセットボタンを横に配置した HBox を作成し、表示する
    hbox = widgets.HBox([dropdown_circle, dropdown_cross, button])
    display(hbox)
    
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()
        self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)           
        
    # イベントハンドラをリセットボタンに結びつける
    button.on_click(on_button_clicked)

    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        fig, ax = plt.subplots(figsize=[size, size])
        fig.canvas.toolbar_visible = False
        fig.canvas.header_visible = False
        fig.canvas.footer_visible = False
        fig.canvas.resizable = False          
        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合のみ処理を行う
            if event.inaxes and self.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                self.move(x, y)                
                self.draw_board(ax)

                # 次の手番の処理を行うメソッドを呼び出す
                self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
                
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     

    self.restart()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

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

```diff_python
-def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
+def play(self, ai, ai_list=None, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
    # ai_list が None の場合は、空の list で置き換える
+   if ai_list is None:
+       ai_list = []
元と同じなので省略

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

__上記__ の __修正後__ に、__下記__ のプログラムで __`play`__ メソッドを __実行__ すると、__実行結果__ のように __正しい処理__ が __行われる__ ことが __確認__ できます。__人間__ VS __人間__ の __実行結果__ は __省略__ します。

なお、__下記__ のプログラムを __同じセルで実行__ すると、__2 つ__ の対戦の __Dropdown の表示__ が __ゲーム盤よりも前__ に __並べて表示__ されるので、__別々のセル__ で __実行__ して下さい。

```python
mb.play(ai=[None, None], gui=True);
```
```python
mb.play(ai=[ai1s, ai2s], gui=True);
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/c8f82816-793c-e3f0-50c0-17b09c372e24.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/debc1035-cdf2-a6df-e629-db70b1b604c4.png" width="200">

### デフォルト引数 `params` の修正

:::note warn
__上記の問題__ は、__下記__ のような __処理__ を __行う際__ に __発生__ します。

* __デフォルト値__ に __list など__ の __ミュータブル__ な __データ__ を __代入__ する
* __関数__ の __ブロック内__ で、__デフォルト引数__ の __要素__ を __変更する処理__ を __行う__
:::

逆に言えば、__上記__ のような __処理__ を __行わない__ 場合は、__上記の問題__ は __発生しません__。例えば、__`play`__ メソッドには、__`params=[{}, {}]`__ のように、__デフォルト値__ に list のような __ミュータブル__ な __値__ が __設定__ された __デフォルト引数__ が __存在__ しますが、__`params`__ は、__関数__ の __ブロックの中__ で、その __要素__ を __変更__ することは __ない__ ので、__このまま__ でも __問題__ は __発生しません__。

ただし、__今後 `params`__ の __要素__ を __変更__ するような __処理__ を __行う可能性__ が __ない__ とは __言い切れません__ し、__要素__ を __変更することがない__ と __勘違い__ している場合が __あるかもしれない__ ので、__ミュータブル__ な __データ__ を __デフォルト値__ に __設定したい__ 場合は、__特別な理由__ が __ない限り__、__常にデフォルト値__ に __`None`__ を __設定__ し、__`None` の場合__ に __設定したい値__ を __代入する__ という __処理__ を __記述したほうが良い__ でしょう。

__下記__ は、__そのよう__ に __`play`__ メソッドを __修正__ した __プログラム__ です。

* __1 行目__：__`params`__ の __デフォルト値__ を __`None`__ に __修正__ する
* __6、7 行目__：__`params`__ が __`None`__ の場合に __元のデフォルト値__ を __代入__ する

```python
1  def play(self, ai, ai_list=None, params=None, verbose=True, seed=None, gui=False, size=3):      
2      # ai_list が None の場合は、空の list で置き換える
3      if ai_list is None:
4          ai_list = []
5      # params が None の場合のデフォルト値を設定する
6      if params is None:
7          params = [{}, {}]
元と同じなので省略
8
9  Marubatsu.play = play
```

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

```python
def play(self, ai, ai_list=None, params=None, verbose=True, seed=None, gui=False, size=3):      
    # ai_list が None の場合は、空の list で置き換える
    if ai_list is None:
        ai_list = []
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # ai_list から、項目だけを取り出した list を作成する
    ai_values = [value for label, value in ai_list]
    # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
    select_values = []
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        # ラベルと項目の値を計算する
        if ai[i] is None:
            label = "人間"
            value = "人間"
        else:
            label = ai[i].__name__        
            value = ai[i]
        # value を select_values に常に登録する
        select_values.append(value)
        # value が ai_values に登録済かどうかを判定する
        if value not in ai_values:
            # 項目を登録する
            ai_list.append((label, value))
            ai_values.append(value)
    
    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_list,
        description="〇",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[0],
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_list,
        description="×",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[1],
    )       
    
    # リセットボタンを作成する
    button = widgets.Button(
        description="リセット",
        layout=widgets.Layout(width="100px"),
    )
    
    # 〇 と × の dropdown と リセットボタンを横に配置した HBox を作成し、表示する
    hbox = widgets.HBox([dropdown_circle, dropdown_cross, button])
    display(hbox)
    
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()
        self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)           
        
    # イベントハンドラをリセットボタンに結びつける
    button.on_click(on_button_clicked)

    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        fig, ax = plt.subplots(figsize=[size, size])
        fig.canvas.toolbar_visible = False
        fig.canvas.header_visible = False
        fig.canvas.footer_visible = False
        fig.canvas.resizable = False          
        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合のみ処理を行う
            if event.inaxes and self.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                self.move(x, y)                
                self.draw_board(ax)

                # 次の手番の処理を行うメソッドを呼び出す
                self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
                
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     

    self.restart()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

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

```diff_python
def play(self, ai, ai_list=None, params=None, verbose=True, seed=None, gui=False, size=3):      
    # ai_list が None の場合は、空の list で置き換える
    if ai_list is None:
        ai_list = []
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
元と同じなので省略

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

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

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

:::note
上記の記事を記述している際に気づいたのですが、これまでは __`play_loop`__ の __いくつか__ の __仮引数__ に __デフォルト値__ を __設定__ していました。__`play_loop`__ は、__`play`__ メソッドの中から、必ず __すべての仮引数__ に __対応__ する __実引数を記述__ して __呼び出している__ ので、__デフォルト値__ を __設定__ する __意味__ が __ありません__。そこで、__下記__ プログラムのように、__`play_loop`__ の __デフォルト引数__ を、__通常__ の __仮引数__ に __修正__ することにします。

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

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.draw_board(ax)
                # 手番を人間が担当する場合は、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.draw_board(ax)
        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=True, gui=False):
+def play_loop(self, ai, ax, params, verbose, gui):
元と同じなので省略

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

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

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

:::note
__細かい話__ になりますが、__上記__ の「__関数__ の __ブロック内__ で、__デフォルト引数__ の __内容__ を __変更する処理__ を __行う__」は、__その関数の中__ で __デフォルト値__ の __内容__ を __変更しなくても__、__デフォルト値__ を __他の関数呼び出し__ の __実引数に記述__ し、__呼び出された関数の中__ で __デフォルト値の内容__ を __変更する__ 場合も __含みます__。

例えば、__下記__ のプログラムは、__`a`__ から __呼び出した__ 関数 __`b` の中__ で __デフォルト値__ に __要素を追加__ するので、__`a`__ を __呼び出すたび__ に、__`x`__ の __デフォルト値__ は __変化__ します。

```python
def a(x=[]):
    print(x)
    b(x)

def b(x):
    x.append(1)

a()
a()
```

実行結果

```
[]
[1]
```
:::

# 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%E8%A7%A3%E6%B1%BA%E6%96%B9%E6%B3%95)で、__循環インポート__ の __エラーを解決__ するために、__`ai1s`__ ~ __`ai14s`__ を __Dropdown__ に __登録する処理__ を __`play`__ メソッドから __削除__ したため、それらの AI を __Dropdown__ で __選択できる__ ようにするためには、__下記__ のプログラムのように、__`ai1s`__ ~ __`ai14s`__ の __データが登録__ された __`ai_list`__ を __作成する処理__ を __記述する必要__ が __あります__。

```python
import ai

# Dropdown の作成の際に必要となる、AI のリストを作成する    
ai_list = []
for i in range(1, 15):
    ai_name = f"ai{i}s"  
    ai_list.append((ai_name, getattr(ai, ai_name)))   

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

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/ddea92ad-6923-e6b5-8652-793a0b645e74.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/adf3dfa2-a720-ffad-a24a-34691ee47051.png" width="200">

__毎回__ このような __処理を記述__ するのは __大変__ なので、__GUI__ で __`ai1s`__ ~ __`ai14s`__ の __14 種類__ の __AI__ を __選択__ する __Dropdown__ を __表示__ して __〇×ゲームを遊ぶ__ ための __下記__ のような __関数を定義__ することにします。

__名前__：__GUI__ で __遊ぶ__（play）ので、__`gui_play`__ とする
__処理__：__GUI__ で __`ai1s`__ ~ __`ai14s`__ の __AI__ を __選択__ する __Dropdown__ を __表示__ して __〇×ゲームを遊ぶ__
__入力__：__最初の担当__ と、__Dropdown__ に __表示する項目__ を __自由__ に __設定できる__ ように、__`play`__ メソッドと __同じ意味__ を持つ __仮引数 `ai`__ と __`ai_list`__ を __持つ__ ようにする
__出力__：なし

## `gui_play` の定義

__下記__ は、__`gui_play`__ の __定義__ です。なお、__`gui_play`__ では、__`ai`__ に __対応__ する __実引数__ を __記述しなかった場合__ に __人間どうし__ の __対戦__ が行われるように __工夫__ してみました。

* __1 行目__：__`gui_play`__ には __`ai`__ という __仮引数__ が __存在する__ ので、__ai モジュール__ を再び __`ai_module`__ という __別の名前__ で __インポート__ する __必要がある__
* __3 行目__：__デフォルト値__ に __`None`__ を __設定__ した __デフォルト引数 `ai`__ と __`ai_list`__ を持つ __`gui_play`__ を定義する
* __5、6 行目__：__`ai`__ に __`None` が代入__ されていた場合は、__`ai`__ に __`[None, None]`__ を __代入__ することで、__人間どうし__ の __対戦__ が行われるようにする
* __8 ~ 12 行目__：__`ai_list`__ に __`None` が代入__ されていた場合は、__`ai_list`__ に __`ai1s`__ ~ __`ai14s`__ の __項目__ を持つ __Dropdown__ を __作成するため__ の __データ__ を __計算__ して __代入__ する
* __14、15 行目__：__`Marubatsu`__ クラスの __インスタンスを作成__ し、__`play`__ メソッドで __GUI__ で __上記の設定__ で __〇×ゲームの対戦__ を __行う__

```python
 1  import ai as ai_module
 2
 3  def gui_play(ai=None, ai_list=None):
 4      # ai が None の場合は、人間どうしの対戦を行う
 5      if ai is None:
 6          ai = [None, None]
 7      # ai_list が None の場合は、ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
 8      if ai_list is None:
 9          ai_list = []
10          for i in range(1, 15):
11              ai_name = f"ai{i}s"  
12              ai_list.append((ai_name, getattr(ai_module, ai_name)))    
13
14      mb = Marubatsu()
15      mb.play(ai=ai, ai_list=ai_list, gui=True)
```

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

```python
import ai as ai_module

def gui_play(ai=None, ai_list=None):
    # ai が None の場合は、人間どうしの対戦を行う
    if ai is None:
        ai = [None, None]
    # ai_list が None の場合は、ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
    if ai_list is None:
        ai_list = []
        for i in range(1, 15):
            ai_name = f"ai{i}s"  
            ai_list.append((ai_name, getattr(ai_module, ai_name)))    
    
    mb = Marubatsu()
    mb.play(ai=ai, ai_list=ai_list, gui=True)
```
</details>

## `gui_play` の動作の確認

__下記__ のプログラムで、__`gui_play`__ に __実引数__ を __記述せず__ に呼び出した場合は、__GUI__ で __人間__ VS __人間__ の __対戦__ が __行われる__ ことが __確認__ できます。

```python
gui_play()
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/79ca9206-b303-08fe-95c9-87cb37a0e59c.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/bf44d7fb-cc15-e056-1049-c214930ccc8f.png" width="200">

また、__下記__ のプログラムで、__人間__ VS __`ai1s`__ の __対戦__ が __行われる__ ことが __確認__ できます。

```python
gui_play(ai=[None, ai1s])
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/9040fc9f-a4b0-10d2-8967-4cf7e6c43c99.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3c616932-2e40-2a31-e10c-4312b1d54e49.png" width="200">

また、__下記__ のプログラムで、__`ai1` のみ__ が __登録__ された __`ai_list`__ のデータを __作成__ して __キーワード引数__ に __記述__ した場合でも __`gui_play`__ が __正しく動作__ することが __確認__ できます。

```python
gui_play(ai_list=[("ai1s", ai.ai1s)])
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/60578ea5-d631-fb2f-14bd-dc75f9ea3641.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/fa6baff0-df30-733c-2d86-20278016466c.png" width="200">

実は、__`gui_play`__ には __問題__ が __あります__。それが何か少し考えてみて下さい。

## 人間の項目の登録

__`gui_play`__ には、__下記__ のプログラムのように、__AI どうし__ の __対戦__ を行う場合、__実行結果__ の __右下図__ のように、__Dropdown__ に __人間の項目__ が __表示されない__ という __問題__ が __あります__。

```python
gui_play(ai=[ai1s, ai1s])
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/f7e720c3-e37c-215d-b560-21556f673aa0.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/0999538b-79da-b50c-5296-d583d0ea01a0.png" width="200">

__先程__、__`gui_play()`__ を __実行__ した際に、__下図__ のように __人間の項目__ が __最後に表示__ されたのは、__人間__ VS __人間__ の __対戦__ が行われたため、__`play`__ メソッドの __中で__、__`ai_list`__ に __登録されていない__ 人間の項目が __自動的__ に __登録された__ からです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/bf44d7fb-cc15-e056-1049-c214930ccc8f.png" width="200">

__この問題__ は、__下記__ のプログラムの __7 行目__ のように、__`gui_play`__ の中の、__`ai_list`__ の __初期化処理__ で、__人間の項目__ の __データ__ を __代入__ することで __解決__ できます。

```python
 1  def gui_play(ai=None, ai_list=None):
 2      # ai が None の場合は、人間どうしの対戦を行う
 3      if ai is None:
 4          ai = [None, None]
 5      # ai_list が None の場合は、人間と ai1s ~ ai14s の Dropdown を作成するためのデータを計算する  
 6      if ai_list is None:
 7          ai_list = [("人間", "人間")]
 8          for i in range(1, 15):
 9              ai_name = f"ai{i}s"  
10              ai_list.append((ai_name, getattr(ai_module, ai_name)))    
11    
12      mb = Marubatsu()
13      mb.play(ai=ai, ai_list=ai_list, gui=True)
```

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

```python
def gui_play(ai=None, ai_list=None):
    # ai が None の場合は、人間どうしの対戦を行う
    if ai is None:
        ai = [None, None]
    # ai_list が None の場合は、人間と ai1s ~ ai14s の Dropdown を作成するためのデータを計算する  
    if ai_list is None:
        ai_list = [("人間", "人間")]
        for i in range(1, 15):
            ai_name = f"ai{i}s"  
            ai_list.append((ai_name, getattr(ai_module, ai_name)))    
    
    mb = Marubatsu()
    mb.play(ai=ai, ai_list=ai_list, gui=True)
```
</details>

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

```diff_python
def gui_play(ai=None, ai_list=None):
    # ai が None の場合は、人間どうしの対戦を行う
    if ai is None:
        ai = [None, None]
    # ai_list が None の場合は、人間と ai1s ~ ai14s の Dropdown を作成するためのデータを計算する  
    if ai_list is None:
-       ai_list = []
+       ai_list = [("人間", "人間")]
        for i in range(1, 15):
            ai_name = f"ai{i}s"  
            ai_list.append((ai_name, getattr(ai_module, ai_name)))    
    
    mb = Marubatsu()
    mb.play(ai=ai, ai_list=ai_list, gui=True)
```
</details>

__上記__ の __修正後__ に、__下記__ のプログラムで __`play`__ メソッドを __実行__ すると、__実行結果__ のように __正しい処理__ が __行われる__ ことが __確認__ できます。なお、__人間の項目__ を __`gui_play` の中__ で __最初に登録__ したので、__先程__ と __異なり__、__人間の項目__ が __最初に表示__ されるようになります。

```python
gui_play(ai=[ai1s, ai1s])
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/f7e720c3-e37c-215d-b560-21556f673aa0.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/d3333dde-04d1-6e99-cd82-5d298d637e32.png" width="200">

## `ai1s` ~ `ai14s` 以外の AI で対戦を行う場合の確認

__下記__ のプログラムで、__`gui_play`__ を使って、__`ai1s`__ ~ __`ai14s` 以外__ の __AI__ で __対戦を行う__ 場合を __確認__ します。__実行結果__ から、__正しい処理__ が行われることが __確認__ できます。

```python
from ai import ai1, ai2

gui_play(ai=[ai1, ai2])
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/bae6dc59-84e1-3d19-6269-51cc4b49a76e.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/07524e09-4909-a769-8515-c39d03250e66.png" width="200">

## `gui_play` の記述場所

__`gui_play`__ を __Marubatsu__ クラスの __メソッド__ として __定義しなかった__ 点が __気になってい__ る人が __いるかもしれません__ が、__`gui_play` の中__ で __ai モジュール__ の __関数__ を __利用している__ ので、__`gui_play`__ を __Marubatsu__ クラスの __メソッド__ として __定義__ すると、[前回の記事](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_play`__ は、__AI__ の __処理を行う__ 関数 __ではない__ ので、__ai モジュール__ の __中に記述__ するのは __少し不自然__ です。そこで、本記事では __`gui_play`__ を __util.py__ という __名前__ の __ファイルに保存する__ ことにします。__util.py の中__ で __marubatsu__ と __ai__ の __両方のモジュール__ を __インポート__ しても、__インポートの循環__ は __起こらない__ ので __エラー__ は __発生しません__。

なお、__util__ とは、__小規模__ で、__補助的__ な __機能__ を持つ __プログラム__ を表す __ユーティリティ__（utility）の __略__ で、比較的 __良く使われるプログラミング用語__ です。

# dict を用いた Dropdown のラベルの設定

[以前の記事](https://qiita.com/ysgeso/items/51842efc6193ed456ebc#ai-%E3%81%AE%E5%90%8D%E5%89%8D%E3%82%92%E8%A1%A8%E3%81%99%E3%83%A9%E3%83%99%E3%83%AB%E3%81%AE%E8%A8%AD%E5%AE%9A)で、__Dropdown__ の __ラベル__ を __設定する方法__ として、__`options` 属性__ の __値__ に __`(ラベル, 項目の値)`__ という __tuple__ を __要素__ として持つ __list__ で __設定__ する方法を __紹介__ しましたが、__それ以外__ にも __`{ "ラベル": 項目の値 }`__ という __dict で設定__ することが __できます__。また、__`dict` で設定__ することで、__`play`__ メソッドの __処理__ を __より簡潔に記述__ することが __できるようになる__ ことに気が付きましたので、その方法を紹介することにします。

[前回の記事](https://qiita.com/ysgeso/items/64de676735836b963712#play-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AE%E6%94%B9%E8%89%AF%E3%81%9D%E3%81%AE-1ai-%E3%81%AE%E5%86%85%E5%AE%B9%E3%81%AE%E8%87%AA%E5%8B%95%E7%99%BB%E9%8C%B2)では、__`ai` の内容__ を __Dropdown__ に __自動登録__ する __処理__ を実装しましたが、その際に、__`ai_list` の中__ に、__`ai`__ の __要素__ が __含まれているか__ を __判定する必要__ が __あります__。

__`ai_list`__ の __要素__ には、__`(ラベル, 項目の値)`__ という __tuple__ が __代入__ されているため、__in 演算子__ を使って __その判定__ を行うことは __できません__。そのため、__`ai_list` の中__ から __項目の値__ を __取り出した list__ を __作成__ して __`ai_values`__ に __代入__ するという __処理__ を、__下記__ のプログラムのように __リスト内包表記__ を用いて行いました。

```python
    # ai_list から、項目だけを取り出した list を作成する
    ai_values = [value for label, value in ai_list]
```

__上記の処理__ は、__`ai_list`__ を、__それぞれの項目__ を __`{ "ラベル": 項目の値 }`__ という __キー__ と __キーの値__ を持つ __dict で設定__ することで、__簡潔に記述__ することが __できます__。

具体的には、__下記__ のプログラムのように、__dict__ の __キーの値__ の __一覧__ を表す __list__ を __計算__ する __`values`__ という __メソッド__ を __利用__ します。

```python 
ai_values = ai_list.values()
```

__`ai_list`__ を __dict で表現__ することで、__`play`__ メソッドを下記のように修正します。なお、__`ai_list`__ という __名前のまま__ では __変__ なので、__`ai_dict`__ という __名前に変更__ しました。

* __1 行目__：仮引数 __`ai_list`__ を __`ai_dict`__ に __変更__ する
* __4 行目__：__`ai_dict`__ が __`None`__ の場合に __代入する値__ を __空の dict__ に __修正__ する
* __14 行目__：この下にあった __`ai_values`__ を __計算__ する __処理__ を __削除__ する
* __18 行目__：__`ai_values`__ の代わりに、__`ai_dict.values()`__ で __判定を行う__ ように __修正__ する
* __20 行目__：__`ai_dict`__ の __`label`__ の __キーの値__ に __`value`__ を __代入__ する
* __21 行目__：この下にあった __`ai_values`__ に __項目を登録__ する __処理__ を __削除__ する
* __3、25、32 行目__：__`ai_list`__ を __`ai_dict`__ に __変更__ する

```python
 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
 2      # ai_dict が None の場合は、空の list で置き換える
 3      if ai_dict is None:
 4          ai_dict = {}
元と同じなので省略
 5      # ai に代入されている内容を ai_dict に追加する
 6      for i in range(2):
 7          # ラベルと項目の値を計算する
 8          if ai[i] is None:
 9              label = "人間"
10              value = "人間"
11          else:
12              label = ai[i].__name__        
13              value = ai[i]
14          # この下にあった、ai_values を計算する処理を削除する
15          # value を select_values に常に登録する
16          select_values.append(value)
17          # value が ai_values に登録済かどうかを判定する
18          if value not in ai_dict.values():
19              # 項目を登録する
20              ai_dict[label] = value
21          # この下にあった、ai_values に value を登録する処理を削除する
22
23      # 〇 と × の Dropdown を作成する   
24      dropdown_circle = widgets.Dropdown(
25          options=ai_dict,
26          description="〇",
27          layout=widgets.Layout(width="100px"),
28          style={"description_width": "20px"},
29          value=select_values[0],
30      )
31      dropdown_cross = widgets.Dropdown(
32          options=ai_dict,
33          description="×",
34          layout=widgets.Layout(width="100px"),
35          style={"description_width": "20px"},
36          value=select_values[1],
37      )   
元と同じなので省略
38
39  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):      
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
    select_values = []
    # ai に代入されている内容を ai_dict に追加する
    for i in range(2):
        # ラベルと項目の値を計算する
        if ai[i] is None:
            label = "人間"
            value = "人間"
        else:
            label = ai[i].__name__        
            value = ai[i]
        # value を select_values に常に登録する
        select_values.append(value)
        # value が ai_values に登録済かどうかを判定する
        if value not in ai_dict.values():
            # 項目を登録する
            ai_dict[label] = value
    
    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_dict,
        description="〇",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[0],
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_dict,
        description="×",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[1],
    )       
    
    # リセットボタンを作成する
    button = widgets.Button(
        description="リセット",
        layout=widgets.Layout(width="100px"),
    )
    
    # 〇 と × の dropdown と リセットボタンを横に配置した HBox を作成し、表示する
    hbox = widgets.HBox([dropdown_circle, dropdown_cross, button])
    display(hbox)
    
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()
        self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)           
        
    # イベントハンドラをリセットボタンに結びつける
    button.on_click(on_button_clicked)

    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        fig, ax = plt.subplots(figsize=[size, size])
        fig.canvas.toolbar_visible = False
        fig.canvas.header_visible = False
        fig.canvas.footer_visible = False
        fig.canvas.resizable = False          
        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合のみ処理を行う
            if event.inaxes and self.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                self.move(x, y)                
                self.draw_board(ax)

                # 次の手番の処理を行うメソッドを呼び出す
                self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
                
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     

    self.restart()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

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

```diff_python
-def play(self, ai, ai_list=None, params=None, verbose=True, seed=None, gui=False, size=3):      
+def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
    # ai_dict が None の場合は、空の list で置き換える
-   if ai_list is None:
+   if ai_dict is None:
-       ai_list = []
+       ai_dict = {}
元と同じなので省略
    # ai に代入されている内容を ai_dict に追加する
    for i in range(2):
        # ラベルと項目の値を計算する
        if ai[i] is None:
            label = "人間"
            value = "人間"
        else:
            label = ai[i].__name__        
            value = ai[i]
-       ai_values = [value for label, value in ai_list]
        # value を select_values に常に登録する
        select_values.append(value)
        # value が ai_values に登録済かどうかを判定する
-       if value not in ai_values:
+       if value not in ai_dict.values():
            # 項目を登録する
-           ai_list.append((label, value))
+           ai_dict[label] = value
-           ai_values.append(value)

    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
-       options=ai_list,
+       options=ai_dict,
        description="〇",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[0],
    )
    dropdown_cross = widgets.Dropdown(
-       options=ai_list,
+       options=ai_dict,
        description="×",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[1],
    )     
元と同じなので省略

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

次に、__`gui_play`__ の __中__ の __`ai_dict`__ を __作成する処理__ を、__下記__ のように __修正__ します。

* __1 行目__：仮引数 __`ai_list`__ を __`ai_dict`__ に __変更__ する
* __7 行目__：__`ai_dict`__ に、__人間の項目__ を表す __dict__ を __代入__ するように __修正__ する
* __10 行目__：__`ai_dict`__ の __`ai_name`__ の __キーの値__ に、__`getattr`__ で __計算__ した __AI の関数__ を __代入__ する
* __6、13 行目__：__`ai_list`__ を __`ai_dict`__ に __変更__ する

```python
 1  def gui_play(ai=None, ai_dict=None):
 2      # ai が None の場合は、人間どうしの対戦を行う
 3      if ai is None:
 4          ai = [None, None]
 5      # ai_dict が None の場合は、ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
 6      if ai_dict is None:
 7          ai_dict = { "人間": "人間"}
 8          for i in range(1, 15):
 9              ai_name = f"ai{i}s"  
10              ai_dict[ai_name] = getattr(ai_module, ai_name)
11    
12      mb = Marubatsu()
13      mb.play(ai=ai, ai_dict=ai_dict, gui=True)
```

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

```python
def gui_play(ai=None, ai_dict=None):
    # ai が None の場合は、人間どうしの対戦を行う
    if ai is None:
        ai = [None, None]
    # ai_dict が None の場合は、ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
    if ai_dict is None:
        ai_dict = { "人間": "人間"}
        for i in range(1, 15):
            ai_name = f"ai{i}s"  
            ai_dict[ai_name] = getattr(ai_module, ai_name)
    
    mb = Marubatsu()
    mb.play(ai=ai, ai_dict=ai_dict, gui=True)
```
</details>

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

```diff_python
-def gui_play(ai=None, ai_list=None):
+def gui_play(ai=None, ai_dict=None):
    # ai が None の場合は、人間どうしの対戦を行う
    if ai is None:
        ai = [None, None]
    # ai_dict が None の場合は、ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
-   if ai_list is None:
+   if ai_dict is None:
-       ai_list = [("人間", "人間")]
+       ai_dict = { "人間": "人間"}
        for i in range(1, 15):
            ai_name = f"ai{i}s"  
-           ai_list.append((ai_name, getattr(ai_module, ai_name)))               
+           ai_dict[ai_name] = getattr(ai_module, ai_name)
    
    mb = Marubatsu()
-   mb.play(ai=ai, ai_list=ai_list, gui=True)
+   mb.play(ai=ai, ai_dict=ai_dict, gui=True)
```
</details>

__上記__ の __修正後__ に、__下記__ のプログラムで、__`gui_play`__ に __実引数__ を __記述せず__ に呼び出した場合は、__GUI__ で __人間__ VS __人間__ の __対戦__ が __正しく行われる__ ことが __確認__ できます。

```python
gui_play()
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/79ca9206-b303-08fe-95c9-87cb37a0e59c.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/bf44d7fb-cc15-e056-1049-c214930ccc8f.png" width="200">

本記事では省略しますが、__余裕がある方__ は先程行った __`gui_play(ai=[ai1s, ai1s])` など__ を __実行__ して __`gui_play`__ が __正しく動作する__ ことを __確認__ してみて下さい。

# 今回の記事のまとめ

今回の記事では、__`play`__ メソッドの __バグの修正__ を行いました。また、その際に、__デフォルト引数__ の __デフォルト値__ に __list など__ の __ミュータブル__ な __データ__ を __代入__ した場合の __注意点__ と __対処法__ について __説明__ しました。

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

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

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

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

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

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

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

# 次回の記事

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