Pythonで〇×ゲームのAIを一から作成する　その75　循環インポートとDropdownの項目の自動登録

# 目次と前回の記事

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

https://qiita.com/ysgeso/items/51842efc6193ed456ebc

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

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

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

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

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

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

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

# 循環インポートのエラーの意味とその対処方法

前回の記事で __作成__ したプログラムを __marubatsu.py__ に __記述__ し、__下記__ のプログラムを実行すると __実行結果__ のような __エラーが発生__ します。今回の記事では、最初にこの __エラーの意味__ と __対処方法__ について __説明__ します。

```python
%matplotlib widget
from marubatsu import Marubatsu

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

実行結果

```
略
File c:\Users\ys\ai\marubatsu\075\ai.py:3
      1 # 3.7 ～ 3.9 の Python のバージョンでエラーが発生しないようにするためのインポート
      2 from __future__ import annotations
----> 3 from marubatsu import Marubatsu, Markpat
      4 from random import choice
      5 from collections import defaultdict

ImportError: cannot import name 'Marubatsu' from partially initialized module 'marubatsu' (most likely due to a circular import) (c:\Users\ys\ai\marubatsu\075\marubatsu.py)
```

:::note
上記のエラーメッセージは、以下のような意味を持ちます。

* __ImportError__
モジュールのインポート（import）に関するエラー
* __cannot import name 'Marubatsu' from partially initialized module 'marubatsu' (most likely due to a circular import)__
部分的に（partially）初期化された（initialized）marubatsu というモジュール（module）から（from）Marubatsu という名前（name）をインポート（import）できない（can not）。循環インポート（circular import）が最も可能性が高い（most likely）原因（due to）である
:::

## 循環インポートとは何か

__上記__ の __エラーメッセージ__ から、__エラーの原因__ が、__循環インポート__（circular import）である __可能性が最も高い__ ことが __わかります__。

__循環インポート__ がどのようなものであるかを説明するために、先程実行した __プログラム__ の __処理の流れ__ を、__順を追って説明__ します。

### marubatsu モジュールのインポート

__下記__ の __先程のプログラム__ は、__2 行目__ で __marubatsu__ モジュールから __`Marubatsu`__ という __クラス__ を __インポート__ します。

```python
%matplotlib widget
from marubatsu import Marubatsu
以下略
```

[以前の記事](https://qiita.com/ysgeso/items/4d55e73d6571fe85aa49#%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%88%E3%81%AE%E4%BB%95%E7%B5%84%E3%81%BF)で説明したように、__モジュール__ を __インポート__ する際には、その __モジュール__ のファイルの中に __記述__ された __プログラムが__ すべて __実行__ されます。それは、__`import marubatsu`__ のように、__モジュールそのもの__ を __インポート__ する場合でも、__`from marubatsu import Marubatsu`__ のように、__モジュールの中__ で __定義__ された __関数__ や __クラス__ などを __インポート__ する場合でも __変わりません__。そのため、__上記__ のプログラムの __2 行目__ を __実行__ すると、__marubatsu.py__ の中に __記述__ された __プログラム__ が __すべて実行__ されます。

__python__ では、__モジュール__ を __インポート__ する処理が行われると、__関数__ や __メソッド__ などの __呼び出し__ と __同様__ に、__モジュール__ に __記述__ された __プログラム__ を __すべて実行__ し、その __処理__ が __完了してから__ プログラムの __続きが実行__ されます。従って、__上記__ の __marubatsu.py__ の __2 行目__ の __`from marubatsu import Marubatsu`__ が __実行__ されると __marubatsu.py__ に __記述__ された __プログラムが実行__ され、その __処理が完了__ してから、__3 行目以降__ の プログラムが __実行__ されます。


### marubatsu モジュールの実行

下記は、__marubatsu.py__ の __先頭に記述__ された、__他のモジュール__ を __インポート__ するプログラムで、__marubatsu.py の中__ で、__ai__ モジュールを __インポート__ していることが __わかります__。

```python
# 3.7 ～ 3.9 の Python のバージョンでエラーが発生しないようにするためのインポート
from __future__ import annotations
略
import ai as ai_module
以下略
```

先程と同様に、__上記__ の __marubatsu.py__ の __4 行目__ の __`import ai as ai_module`__ が __実行__ されると __ai.py__ に __記述__ された __プログラムが実行__ され、その __処理が完了__ してから、__marubatsu.py__ の __5 行目以降__ の プログラムが __実行__ されます。


### ai モジュールの実行

下記は、__ai.py__ の __先頭に記述__ された、__他のモジュール__ を __インポート__ するプログラムで、__ai.py の中__ で、__marubatsu__ モジュールを __インポート__ していることが __わかります__。

```python
# 3.7 ～ 3.9 の Python のバージョンでエラーが発生しないようにするためのインポート
from __future__ import annotations
from marubatsu import Marubatsu, Markpat
以下略
```

[以前の記事](https://qiita.com/ysgeso/items/4d55e73d6571fe85aa49#%E8%A4%87%E6%95%B0%E5%9B%9E%E3%81%AE%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%88)で、__Python__ では、__インポートしたモジュール__ を __もう一度インポート__ しようとした場合は、その __モジュールのプログラム__ が __実行されることはない__ と説明しましたが、それは、__インポート__ した __モジュールのプログラム__ が __完全に実行された場合__ です。

__最初に実行__ した __`from marubatsu import Marubatsu`__ によって __実行__ された __marubatsu.py__ のプログラムは、__実行__ の __途中__ で __ai__ モジュールを __インポート__ する __処理を行った__ ため、__上記__ のプログラムの __3 行目__ の __`from marubatsu import Marubatsu, Markpat`__ を __実行する時点__ では __処理__ が __完了していません__。そのため、上記の __3 行目__ を __実行__ すると、__marubatsu__ モジュールを __インポート__ するために、__marubatsu.py__ のプログラムが __実行する必要__ が __生じます__。

### インポートの無限の繰り返し

__実際__ には、__先程__ の __エラーが発生__ するので、__marubatsu__ モジュールが __もう一度実行__ されることは __ありません__ が、__仮に 2 度目__ の  __marubatsu__ モジュールが __実行された場合__ に __どのようなことが起きるか__ について __説明__ します。

__2 度目__ の __marubatsu__ モジュールが __実行__ された場合は、__1 度目__ と __同様__ に、__`import ai as ai_module`__ によって __ai__ モジュールを __インポート__ する __必要__ が __生じます__ が、__1 度目__ の __ai__ モジュールの __ai.py__ の __実行処理__ も、__途中__ で __marubatsu__ モジュールを __インポートする処理__ が __行われた__ ため __完了していません__。そのため、__ai__ モジュールを __インポート__ するために、__もう一度__ の __ai.py__ を __実行__ する __必要__ が __生じます__。__以後__ は __同様の処理__ が __繰り返し__ 行われます。

__モジュール A__ から __モジュール B__ を __インポートする処理__ を 「__A → B__」のように __表記__ した場合、__上記の処理__ は、 「__marubatsu__ → __ai__ → __marubatsu__ → __ai__ → __marubatsu__ → ・・・」のように、__インポートの処理__ が __循環__ するため、__無限__ に __繰り返される__ ことになります。これが __循環インポート__ で、__Python__ では __循環インポート__ が起きた場合は __ImportError__ を __発生__ させて、__プログラム__ を __強制的に終了__ するという __仕組み__ になっています。

:::note
循環インポートのような、__参照が循環__ する __状況__ のことを __循環参照__ と呼びます。
:::

:::note warn
__循環インポート__ は、__以下__ のような場合に __発生__ する。

__モジュール A__ が __別のモジュール B__ を __インポートする__ という __処理__ を「__A__ → __B__」と __表記__ した場合に、「__A__ → __B__ →・・・→ __A__」のように、__インポート__ した __モジュール__ を __辿っていく__ と、__元のモジュール__ に __戻る__ という __循環が存在する__。
:::

:::note
__Python__ では、__モジュール__ を __インポートする際__ に行われる __処理__ を、__モジュール__ を __初期化する__（initalize）と呼びます。__先程__ の __プログラム__ を __実行__ した際に __表示__ された __partially initialized module__ という __エラーメッセージ__ は、__モジュール__ が __部分的__（partially）に __初期化__ されている（initialized）、すなわち、__モジュール__ の __インポート__ の __処理__ が __完了していない__ という __意味__ を表します。
:::

:::note
__このノート__ は、__細かい話__ なので、__意味がわからない__ 場合は __無視して下さい__。

__循環インポート__ が __あっても__、__インポート__ した __モジュール__ の __名前空間__ の __名前__ を __利用しなければ__、__エラー__ は __発生しません__。例えば、__下記__ のような、__お互い__ を __インポート__ する __test.py__ と __test2.py__ があった場合に、__`import test`__ で __test__ モジュールを __インポート__ しても、それらの __モジュールの中__ で、__インポートしたモジュール__ の __名前空間__ の __名前__ を __参照していない__ ので __エラー__ は __発生しません__。

```python:test.py
import test2
```

```python:test2.py
import test
```

```python
import test
```

実行結果（エラーは発生せず、何も表示されない）

```
```

一方、__下記__ の __お互い__ を __インポート__ する __test3.py__ と __test4.py__ は、その中で __インポート__ した __モジュール__ の __名前空間__ の __名前を参照する__ ので、__`import test3`__ を __実行__ すると __循環インポート__ を表す __ImportError__ の __エラーが発生__ します。このようなことが起きるのは、__モジュール内__ で __定義__ された __名前を利用__ するための __名前空間__ が __完成__ するのは、__モジュール__ の __実行の処理__ が __完了した時点__ だからです。

```python:test3.py
import test4
a = 1
print(test4.a)
```

```python:test4.py
import test3
a = 2
print(test3.a)
```

```python
import test3
```

実行結果

```
略
File c:\Users\ys\ai\marubatsu\075\test4.py:3
      1 import test3
      2 a = 2
----> 3 print(test3.a)

AttributeError: partially initialized module 'test3' has no attribute 'a' (most likely due to a circular import)
```

:::

## 循環インポートの解決方法

__循環インポート__ の __エラーを修正__ するためには、__循環インポート__ が __起きないよう__ に __プログラム__ を __記述する必要__ が __あります__。今回の場合は、 「__marubatsu__ → __ai__ → __marubatsu__ → ・・・」という __循環インポート__ が __発生__ してるので、__以下__ の __いずれかの修正__ を行うことで、__循環インポート__ が __発生しなくなります__。

* __marubatsu__ モジュールから __ai__ モジュールを __インポートしない__
* __ai__ モジュールから __marubatsu__ モジュールを __インポートしない__

__ai__ モジュール内のプログラムは、__marubatsu__ モジュールで __定義__ された __`Marubatsu`__ クラスを __利用する必要__ が __ある__ ので、どうしても __marubatsu__ モジュールを __インポート__ する __必要__ が __あります__。そのため、__循環インポート__ を __解消__ するためには、__marubatsu__ モジュールから、__ai__ モジュールを __インポートしない__ ようにする __必要__ が __あります__。

__marubatsu__ モジュール内で __ai__ モジュールを __利用__ しているのは、[前回の記事](https://qiita.com/ysgeso/items/51842efc6193ed456ebc#play-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AE%E4%BF%AE%E6%AD%A3)で __ai__ モジュール内で __定義__ された __AI__ を __Dropdown__ で __選択できる__ ように __修正__ した __`play`__ メソッドです。__下記__ は、__`play`__ メソッド内で __ai__ モジュールを __利用する処理__ の部分のプログラムです。具体的には、__5 行目__ の __`ai_module`__ が __インポート__ した __ai__ モジュールを __表します__。

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

__上記__ の __処理__ を __`play`__ メソッドから __削除__ することで、__marubatsu__ モジュール内で __ai__ モジュールを __インポート__ する __必要__ が __なくなります__。ただし、__この処理__ を __`play`__ メソッドから __削除__ してしまうと、__Dropdown__ に __登録__ する __AI の一覧__ の __情報__ が __なくなってしまう__ ので、その代わりに __下記__ のプログラムのように、__`play`__ メソッドの __仮引数__ に __`ai_list` を追加__ し、__`play`__ メソッドを __実行する際__ に、__Dropdown__ に __登録__ する __AI の一覧__ を __実引数__ で記述して __指定する__ ように __修正__ することにします。その際に、これまでのプログラムとの __[互換性を考慮](https://qiita.com/ysgeso/items/af8b5dda0ddffd5c6d13#%E9%96%A2%E6%95%B0%E3%81%AE%E3%83%87%E3%83%95%E3%82%A9%E3%83%AB%E3%83%88%E5%BC%95%E6%95%B0%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%A6%E4%BA%92%E6%8F%9B%E6%80%A7%E3%82%92%E6%8C%81%E3%81%A4)__ して、__`ai_list`__ は、__空の list__ を __デフォルト値__ とした __デフォルト引数__ とします。

* __1 行目__：__仮引数__ に、__デフォルト値__ に __空の list__ を __設定__ した __`ai_list`__ を __追加__ する
* __`ai_list`__ を __作成する処理__ を __削除__ する

```python
1  def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
2      # seed が None でない場合は、seed を乱数の種として設定する
3      if seed is not None:
4          random.seed(seed)
5
6      # この下にあった、ai_list を作成する処理を削除する
元と同じなので省略
```

<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)
        
        # 〇 と × の Dropdown を作成する   
        dropdown_circle = widgets.Dropdown(
            options=ai_list,
            description="〇",
            layout=widgets.Layout(width="100px"),
            style={"description_width": "20px"},
        )
        dropdown_cross = widgets.Dropdown(
            options=ai_list,
            description="×",
            layout=widgets.Layout(width="100px"),
            style={"description_width": "20px"},
        )       
        
        # リセットボタンを作成する
        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)
```
</details>

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

```diff_python
-   def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
+   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)

        # Dropdown の作成の際に必要となる、AI のリストを作成する    
-       ai_list = []
-       for i in range(1, 15):
-           ai_name = f"ai{i}s"  
-           ai_list.append((ai_name, getattr(ai_module, ai_name)))    
元と同じなので省略
```
</details>

__marubatsu__ モジュールを __インポート__ すると __エラーが発生__ するため、__これまで__ のように、__上記__ のプログラムを __記述した後__ で、__`Marubatsu.play = play`__ を __実行__ するという方法で __`play`__ メソッドを __修正__ することは __できません__。そこで、今回の記事では、__marubatsu.py__ から __ai__ モジュールを __インポート__ する __`import ai as ai_module`__ を __削除__ し、__`play`__ メソッドを __上記__ のように __修正__ した __marubatsu_correct.py__ という __ファイルを保存__ することにします。__次回の記事__ からは、__これまでと同様__ に __marubatsu.py__ というファイルに __保存__ します。

__下記__ は、__そのファイル__ を __保存後__ に、__`ai_list`__ を __作成する処理__ を __記述__ し、__`play`__ メソッドの __実引数に記述__ して __実行__ するプログラムです。__実行結果__ から、__正しく__ プログラムが __動作する__ ことが __確認できます__。

* __1 行目__：__marubatsu_correct__ から __`Marubatsu`__ クラスを __インポート__ する
* __2 行目__：__ai__ モジュールを __インポート__ する。なお、__`play`__ メソッドの __外__ で __`ai_list` を作成__ するため、__ai__ モジュールと、__`play`__ メソッドの __仮引数 `ai`__ の __名前__ が __かぶらなくなった__ ので、__`import ai as ai_module`__ のように __名前を変えてインポート__ する __必要はない__
* __5 ~ 8 行目__：__`play`__ メソッド内で __記述__ していたのと __同じ方法__ で __`ai_list`__ を __作成__ する
* __11 行目__：__`play`__ メソッドの __実引数__ に __`ai_list=ai_list`__ を __記述__ して __呼び出す__

```python
 1  from marubatsu_correct import Marubatsu
 2  import ai
 3
 4  # Dropdown の作成の際に必要となる、AI のリストを作成する    
 5  ai_list = []
 6  for i in range(1, 15):
 7      ai_name = f"ai{i}s"  
 8      ai_list.append((ai_name, getattr(ai, ai_name)))    
 9   
10  mb = Marubatsu()
11  mb.play(ai=[None, None], ai_list=ai_list, gui=True)
```

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

```python
from marubatsu_correct import Marubatsu
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 = Marubatsu()
mb.play(ai=[None, None], ai_list=ai_list, gui=True)
```
</details>

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3bc4ad5f-ec6e-a828-c8a7-8ae63221beee.png" width="200">

# `play` メソッドの改良その 1（`ai` の内容の自動登録）

[前回の記事](https://qiita.com/ysgeso/items/51842efc6193ed456ebc#play-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AE%E4%BF%AE%E6%AD%A3)では __`play`__ メソッドを、__`ai1s`__ ~ __`ai14s`__ の __14 種類__ の __AI__ を __選択できる__ ように __実装__ しましたが、一方で __それ以外の AI__ を __選択できない__ という __欠点__ があります。

それに対し、__先程修正__ した __`play`__ メソッドは、好きな __AI のリスト__ を __作成__ して __実引数に記述__ することで、__任意の AI__ を __選択できる__ ようになったという __利点__ があります。しかし、__その一方__ で、__下記__ のプログラムのように __`ai_list`__ に __対応__ する __実引数__ を __記述せず__ に __`play`__ メソッドを呼び出すと、__Dropdown__ の __`options`__ 属性に __空の list__ が __代入__ されるため、__下図左__ の __実行結果__ のように、__Dropdown__ に __何も表示されなく__ なります。また、__Dropdown の上__ で __マウスを押す__ と、__下図右__ のように、__選択できる項目がない__ メニューが __表示__ されます。

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

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/8e048323-888a-ac1a-c6a8-1d10ac529a77.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/af27841e-790a-774d-cf38-02f5f642d769.png" width="200">

__Dropdown__ の __項目__ に __何も表示されない__ のは __変__ なので、仮引数 __`ai`__ に __代入__ された __内容__ を __Dropdown__ に __自動的に登録__ するように __`play`__ メソッドを __修正__ することにします。

## 人間の登録

__先程__ の __下記__ のプログラムは、__人間どうし__ の __対戦__ を行うものですが、__前回の記事__ で __作成__ した __Dropdown__ には __AI のみ__ が __登録__ されていました。

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

### 人間を表す Dropdown の項目の設定

そのため、__Dropdown__ に __人間を表す項目__ を __どのように登録するか__ について __決める必要__ が __あります__。__Dropdown の項目__ には、メニューに __表示する文字__ を表す __ラベル__ と、__項目の値__ を __決める必要__ が __あります__。本記事では __ラベル__ は __"人間"__ とすることにします。他の文字が良いと思った人は自由に変更して下さい。__項目の値__ は、__`play`__ メソッドの __仮引数 `ai`__ では __人間__ の担当を __`None`__ と __表していた__ ので、そのまま __`None` を設定__ することにします。

:::note alert
実は、__Dropdown__ の __項目の値__ に __`None`__ を __設定するべき__ では __ありません__ が、筆者はそのことに気づかずに最初は `None` を設定し、後からおかしいことに気づきました。__Dropdown__ の __仕組み__ を __理解する__ 上で、この __間違いの体験__ は __役に立つ__ と __思いました__ ので、__あえて最初__ は __Dropdown__ の __項目の値__ に __`None`__ を __設定__ しました。
:::

### `play` メソッドの修正

__下記__ は、仮引数 __`ai`__ の __要素__ に __人間を表す `None`__ が __代入__ されていた場合に、__`ai_list`__ に __人間を表す項目__ を __追加__ するように __`play`__ メソッドを __修正__ したプログラムです。

* __11 ~ 13__ 行目：__繰り返し処理__ によって、__`ai`__ の __各要素__ に __`None` が代入__ されている場合に、__`ai_list`__ に __人間の項目__ を __追加__ する

```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      # seed が None でない場合は、seed を乱数の種として設定する
 7      if seed is not None:
 8          random.seed(seed)
 9   
10      # ai に代入されている内容を ai_list に追加する
11      for i in range(2):
12          if ai[i] is None:
13              ai_list.append(("人間", None))
元と同じなので省略
14
15  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 に代入されている内容を ai_list に追加する
    for i in range(2):
        if ai[i] is None:
            ai_list.append(("人間", None))
    
    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_list,
        description="〇",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_list,
        description="×",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
    )       
    
    # リセットボタンを作成する
    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):      
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # ai に代入されている内容を ai_list に追加する
+   for i in range(2):
+       if ai[i] is None:
+           ai_list.append(("人間", None))
元と同じなので省略

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

__上記__ の __修正後__ に、__下記__ のプログラムで __`play`__ メソッドを __実行__ すると、__実行結果__ の __下図左__ のように、__Dropdown__ に __人間が表示__ されるようになります。しかし、__Dropdown の上__ で __マウスを押す__ と、__下図右__ のように __人間__ が __2 つ登録__ されるという __問題が発生__ します。このようなことがおきる原因について少し考えてみて下さい。

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

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/24d74af9-de88-622b-6fee-7629a85cddb1.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/79758b7d-557d-8a98-eb51-442a9b542804.png" width="200">

### 項目の重複登録の原因と修正

__先程__ のプログラムは、__繰り返し処理__ によって、__`ai`__ の __各要素__ が __`None`__ の場合に __人間の項目__ を __登録__ する処理を行います。そのため、__人間__ VS __人間__ の場合は、__2 回分__ の __人間の項目__ が __登録__ されてしまいます。この __問題を解決__ するためには、__`ai_list`__ に既に __登録済の項目__ を __重複__ して __登録しない__ ようにする __必要__ が __あります__。

__list__ の __要素の中__ に、__特定の値__ が __含まれているか__ どうかは、__`in`__ という __演算子__ で __判定__ することが __できます__ が、__`ai_list`__ の __要素__ に __人間を表す項目__ が __含まれているか__ どうかを __`None in ai_list`__ で __判定__ することは残念ながら __できません__。その __理由__ は、__`ai_list`__ の __要素__ が、__`(ラベル, 項目の値)`__ という __tuple__ だからです。__`ai_list`__ の中から、__項目の値__ だけを __取り出した list__ を __作成__ することで、__`in`__ 演算子を使って __判定できる__ ようになります。

下記は、__`play`__ メソッドで __人間の項目__ を __重複__ して __登録しないよう__ にするプログラムです。

* __7 行目__：__list 内包表記__ を使って、__`ai_list`__ の __各要素__ から __項目の値__ を __取り出した list__ を __作成__ し、__`ai_values`__ に __代入__ する
* __10 行目__：__条件式__ に「__`None`__ が __`ai_values`__ に __登録されていない__」という __条件を追加__ する
* __12 行目__：__`ai_values`__ に、__`None`__ を __追加__ する

__`ai_list`__ と __`ai_values`__ は __異なる list__ なので、__12 行目__ で __`ai_values`__ に __`None`__ を __追加__ する __処理__ を __忘れないよう__ に __注意__ して下さい。

```python
 1  def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
 2      # seed が None でない場合は、seed を乱数の種として設定する
 3      if seed is not None:
 4          random.seed(seed)
 5   
 6      # ai_list から、項目だけを取り出した list を作成する
 7      ai_values = [value for label, value in ai_list]
 8      # ai に代入されている内容を ai_list に追加する
 9      for i in range(2):
10          if ai[i] is None and None not in ai_values:
11              ai_list.append(("人間", None))
12              ai_values.append(None)
元と同じなので省略
13
14  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]
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        if ai[i] is None and None not in ai_values:
            ai_list.append(("人間", None))
            ai_values.append(None)
    
    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_list,
        description="〇",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_list,
        description="×",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
    )       
    
    # リセットボタンを作成する
    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):      
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # ai_list から、項目だけを取り出した list を作成する
+   ai_values = [value for label, value in ai_list]
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
-       if ai[i] is None:
+       if ai[i] is None and None not in ai_values:
            ai_list.append(("人間", None))
            ai_values.append(None)
元と同じなので省略

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

__上記__ の __修正後__ に、__下記__ のプログラムで __`play`__ メソッドを __実行__ し、__Dropdown の上__ で __マウスを押す__ と、__下図右__ のように __人間__ が __1 つだけ表示__ されることが __確認__ できます。

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

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/24d74af9-de88-622b-6fee-7629a85cddb1.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3b82717b-1a66-a5a1-c955-fec5aafc91be.png" width="200">

## AI の登録

次は、__AI__ を __自動的に登録__ することにします。__AI__ の __ラベル__ は、__AI__ の __関数名__ を __設定__ すれば良く、__関数の名前__ は [以前の記事](https://qiita.com/ysgeso/items/93fb9b6daa59149514e9#%E8%A1%A8%E7%A4%BA%E3%81%AE%E6%94%B9%E8%89%AF%E3%81%9D%E3%81%AE-5ai-%E3%81%AE%E5%90%8D%E5%89%8D%E3%81%AE%E8%A1%A8%E7%A4%BA)で説明したように、__`__name__`__ という __特殊属性__ に __代入__ されています。従って、__`play`__ メソッドの __該当する部分__ を __下記__ のように __修正__ します。

* __6 行目__：__人間でなく__、__`ai[i]`__  が __`ai_values`__ に __登録されていない__ ことを __判定__ する
* __7、8 行目__：__`ai_list`__ と __`ai_values`__ に __AI のデータ__ を __登録__ する

```python
1  # ai に代入されている内容を ai_list に追加する
2  for i in range(2):
3      if ai[i] is None and None not in ai_values:
4          ai_list.append(("人間", None))
5          ai_values.append(None)
6      elif ai[i] is not None and ai[i] not in ai_values:
7          ai_list.append((ai[i].__name__, ai[i]))
8          ai_values.append(ai[i])        
```

__上記__ のプログラムは、__3 ~ 5 行目__ と __6 ~ 8 行目__ の __重複する処理__ を __まとめる__ ことが __できます__。どのようにすれば良いかについて少し考えてみて下さい。

### 重複する処理の統合

__上記__ のプログラムの __3 行目__ の __条件式__ が __`True`__ になる __場合__ は、__`ai[i]`__ の値が __`None`__ になります。従って、__上記__ のプログラムは、__下記__ のように __記述__ することができます。

* __3 ~ 5 行目__：__`None`__ を __`ai[i]`__ に __修正__ する

```python
1  # ai に代入されている内容を ai_list に追加する
2  for i in range(2):
3      if ai[i] is None and ai[i] not in ai_values:
4          ai_list.append(("人間", ai[i]))
5          ai_values.append(ai[i])
6      elif ai[i] is not None and ai[i] not in ai_values:
7          ai_list.append((ai[i].__name__, ai[i]))
8          ai_values.append(ai[i])        
```

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

```python
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        if ai[i] is None and ai[i] not in ai_values:
            ai_list.append(("人間", ai[i]))
            ai_values.append(ai[i])
        elif ai[i] is not None and ai[i] not in ai_values:
            ai_list.append((ai[i].__name__, ai[i]))
            ai_values.append(ai[i])       
```
</details>

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

```diff_python
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
-       if ai[i] is None and None not in ai_values:
+       if ai[i] is None and ai[i] not in ai_values:
-           ai_list.append(("人間", None))
+           ai_list.append(("人間", ai[i]))
-           ai_values.append(None)
+           ai_values.append(ai[i])
        elif ai[i] is not None and ai[i] not in ai_values:
            ai_list.append((ai[i].__name__, ai[i]))
            ai_values.append(ai[i])       
```
</details>

__上記__ の __3 行目__ と __6 行目__ の __条件式__ は、__いずれ__ も __`and` の後ろ__ に __`ai[i] not in ai_values`__ が __記述__ されているので、__どちらのブロック__ も __`ai[i] not in ai_values`__ が __`True`__ になる __場合のみ実行__ されます。従って、__上記__ のプログラムは、__下記__ のように __記述__ できます。

* __3 行目__：__`ai[i]`__ が __`ai_values`__ に __含まれているか__ どうかを __判定__ する
* __4 ~ 6 行目__：__人間が担当__ する場合に、__人間の項目__ を __登録__ する
* __7 ~ 9 行目__：__AI が担当__ する場合に、__AI の項目__ を __登録__ する

```python
1  # ai に代入されている内容を ai_list に追加する
2  for i in range(2):
3      if ai[i] not in ai_values:
4          if ai[i] is None:
5              ai_list.append(("人間", None))
6              ai_values.append(ai[i])
7          elif ai[i] is not None:
8              ai_list.append((ai[i].__name__, ai[i]))
9              ai_values.append(ai[i])        
```

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

```python
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        if ai[i] not in ai_values:
            if ai[i] is None:
                ai_list.append(("人間", None))
                ai_values.append(ai[i])
            elif ai[i] is not None:
                ai_list.append((ai[i].__name__, ai[i]))
                ai_values.append(ai[i])    
```
</details>

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

```diff_python
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
-       if ai[i] is None and ai[i] not in ai_values:
-           ai_list.append(("人間", ai[i]))
-           ai_values.append(ai[i])
+       if ai[i] not in ai_values:
+           if ai[i] is None:
+               ai_list.append(("人間", None))
+               ai_values.append(ai[i])
-       elif ai[i] is not None and ai[i] not in ai_values:
-           ai_list.append((ai[i].__name__, ai[i]))
-           ai_values.append(ai[i])  
+           elif ai[i] is not None:
+              ai_list.append((ai[i].__name__, ai[i]))
+               ai_values.append(ai[i])    
```
</details>

__上記__ の __7 行目__ の __条件式__ の __`ai[i] is not None`__ は、__4 行目__ の __`ai[i] is None`__ が __`False`__ の場合に __実行__ されるので、__その場合__ は __必ず `True`__ になります。従って、__上記__ のプログラムは __下記__ のように __記述__ できます。

* __7 行目__：__`else`__ に __修正__ する

```python
1  # ai に代入されている内容を ai_list に追加する
2  for i in range(2):
3      if ai[i] not in ai_values:
4          if ai[i] is None:
5              ai_list.append(("人間", ai[i]))
6              ai_values.append(ai[i])
7          else:
8              ai_list.append((ai[i].__name__, ai[i]))
9              ai_values.append(ai[i])        
```

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

```python
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        if ai[i] not in ai_values:
            if ai[i] is None:
                ai_list.append(("人間", ai[i]))
                ai_values.append(ai[i])
            else:
                ai_list.append((ai[i].__name__, ai[i]))
                ai_values.append(ai[i]) 
```
</details>

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

```diff_python
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        if ai[i] not in ai_values:
            if ai[i] is None:
                ai_list.append(("人間", ai[i]))
                ai_values.append(ai[i])
-           elif ai[i] is not None:
+           else:
                ai_list.append((ai[i].__name__, ai[i]))
                ai_values.append(ai[i]) 
```
</details>

__上記__ の __5、6 行目__ の __ブロック__ と、__8、9 行目__ の __ブロック__ の処理で __異なる__ のは、__項目__ の __ラベルだけ__ なので、__上記__ のプログラムは、__下記__ のように __記述__ できます。

* __5、7 行目__：__条件式__ の __ブロック内__ で行う __処理__ を、__ラベルの計算だけ__ にする
* __8、9 行目__：__`ai_list`__ と __`ai_values`__ の __要素の追加__ を、__ブロックの外__ で __行う__ ようにする

```python
1  # ai に代入されている内容を ai_list に追加する
2  for i in range(2):
3      if ai[i] not in ai_values:
4          if ai[i] is None:
5              label = "人間"
6          else:
7              label = ai[i].__name__        
8          ai_list.append((label, ai[i]))
9          ai_values.append(ai[i])
```

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

```python
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        if ai[i] not in ai_values:
            if ai[i] is None:
                label = "人間"
            else:
                label = ai[i].__name__        
            ai_list.append((label, ai[i]))
            ai_values.append(ai[i])
```
</details>

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

```diff_python
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        if ai[i] not in ai_values:
+           if ai[i] is None:
-               ai_list.append(("人間", ai[i]))
-               ai_values.append(ai[i])
+               label = "人間"
+           else:
-               ai_list.append((ai[i].__name__, ai[i]))
-               ai_values.append(ai[i]) 
+               label = ai[i].__name__        
+           ai_list.append((label, ai[i]))
+           ai_values.append(ai[i])
```
</details>

上記が __重複する処理__ を __まとめた__ プログラムです。__下記__ の __まとめる前__ のプログラムと __比べる__ と、__行数__ が __1 行増えています__ が、「__条件式__ が __簡潔__ になっている」、「__重複する処理__ が __まとめられている__」という点で __元のプログラム__ より __わかりやすく__ なっています。

```python
1  # ai に代入されている内容を ai_list に追加する
2  for i in range(2):
3      if ai[i] is None and None not in ai_values:
4          ai_list.append(("人間", None))
5          ai_values.append(None)
6      elif ai[i] is not None and ai[i] not in ai_values:
7          ai_list.append((ai[i].__name__, ai[i]))
8          ai_values.append(ai[i])        
```

### `play` メソッドの修正

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

```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]
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        # ai[i] が ai_values に登録済かどうかを判定する
        if ai[i] not in ai_values:
            # ラベルを計算する
            if ai[i] is None:
                label = "人間"
            else:
                label = ai[i].__name__        
            # 項目を登録する
            ai_list.append((label, ai[i]))
            ai_values.append(ai[i])
元と同じなので省略

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]
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        # ai[i] が ai_values に登録済かどうかを判定する
        if ai[i] not in ai_values:
            # ラベルを計算する
            if ai[i] is None:
                label = "人間"
            else:
                label = ai[i].__name__        
            # 項目を登録する
            ai_list.append((label, ai[i]))
            ai_values.append(ai[i])
    
    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_list,
        description="〇",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_list,
        description="×",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
    )       
    
    # リセットボタンを作成する
    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):      
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # ai_list から、項目だけを取り出した list を作成する
    ai_values = [value for label, value in ai_list]
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        # ai[i] が ai_values に登録済かどうかを判定する
-       if ai[i] is None and None not in ai_values:
-           ai_list.append(("人間", None))
-           ai_values.append(None)
-       elif ai[i] is not None and ai[i] not in ai_values:
-           ai_list.append((ai[i].__name__, ai[i]))
-           ai_values.append(ai[i])       
+       if ai[i] not in ai_values:
            # ラベルを計算する
+           if ai[i] is None:
+               label = "人間"
+           else:
+               label = ai[i].__name__        
            # 項目を登録する
+           ai_list.append((label, ai[i]))
+           ai_values.append(ai[i])
元と同じなので省略

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

__上記__ の __修正後__ に、__下記__ のプログラムで __`play`__ メソッドを __実行__ すると、__実行結果__ の __下図左__ のように、__Dropdown__ に __人間のみが表示__ されますが、__Dropdown の上__ で __マウスを押す__ と、__下図右__ のように __人間__ と __ai1s__ が __表示__ されることが __確認__ できます。

```python
from ai import ai1s

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

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/24d74af9-de88-622b-6fee-7629a85cddb1.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/d6ffc457-fe63-6522-2b81-a2edb1eb79e1.png" width="200">

# `play` メソッドの改良その 2（Dropdown の初期選択）

__上記__ のプログラムでは、__人間__ VS __`ai1s`__ の __対戦__ を行っていますが、__実行結果__ からわかるように、__両方__ の __Dropdown__ に __"人間"__ が __表示される__ という __問題__ があります。これは、[前回の記事](https://qiita.com/ysgeso/items/51842efc6193ed456ebc#valueindexlabel)で説明したように、__Dropdown__ の __作成時__ に __`value`__、__`index`__、__`label`__ の __いずれの属性__ も __設定しない__ 場合は、__先頭の項目__ が __選択状態になる__ からです。

そこで、__下記__ のプログラムのように、__項目の値__ を表す __`value`__ 属性に、__それぞれ__ の __`ai` の要素__ を __代入__ することで、__適切な項目__ が __Dropdown__ に __選択される__ ようにします。

* __8、15 行目__：__`Dropdown`__ の __キーワード引数 `value`__ に __それぞれの手番__ の __`ai` の要素__ を __記述__ することで、__Dropdown__ の __作成時__ に __`value`__ 属性に __値を代入__ する

```python
 1  def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2      # 〇 と × の Dropdown を作成する   
 3      dropdown_circle = widgets.Dropdown(
 4          options=ai_list,
 5          description="〇",
 6          layout=widgets.Layout(width="100px"),
 7          style={"description_width": "20px"},
 8          value=ai[0],
 9      )
10      dropdown_cross = widgets.Dropdown(
11          options=ai_list,
12          description="×",
13          layout=widgets.Layout(width="100px"),
14          style={"description_width": "20px"},
15          value=ai[1],
16      )          
元と同じなので省略
17
18  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]
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        # ai[i] が ai_values に登録済かどうかを判定する
        if ai[i] not in ai_values:
            # ラベルを計算する
            if ai[i] is None:
                label = "人間"
            else:
                label = ai[i].__name__        
            # 項目を登録する
            ai_list.append((label, ai[i]))
            ai_values.append(ai[i])
    
    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_list,
        description="〇",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=ai[0],
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_list,
        description="×",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=ai[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):      
元と同じなので省略
    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_list,
        description="〇",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
+       value=ai[0],
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_list,
        description="×",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
+       value=ai[1],
    )          
元と同じなので省略

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

__上記__ の __修正後__ に、__下記__ のプログラムで __`play`__ メソッドを __実行__ すると、__実行結果__ の __下図左__ のように、__×__ の __Dropdown__ には __ai1s__ が __正しく表示__ されますが、__〇__ の __Dropdown__ には __空白__ が __表示__ されてしまいます。ただし、__〇__ の __Dropdown の上__ で __マウスを押す__ と、__下図右__ のように __人間__ と __ai1s__ が __正しく表示__ されることが __確認__ できます。

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

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/f639319b-4108-8ce4-a2a0-6d66c2eb34a8.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/040bc3f6-cd01-2c8a-731a-d355a2c6eb7c.png" width="200">

## Dropdown の `value` 属性の `None` の意味

このようなことが起きる __原因__ は、__`value`__ 属性に __`None`__ が __代入__ された場合は、__どの項目__ も __選択されていない__ という __意味を表す__ ように __Dropdown__ が __設計されている__ からです。

そのため、__上記__ のプログラムで __人間__ が __手番を担当__ する場合の __項目の値__ を __`None`__ に __設定__ した場合のように、__Dropdown__ の __項目の値__ に __`None` が設定__ されている場合では、__`value`__ 属性に __`None` を代入__ すると、__`None`__ が __設定された項目__ が __選択状態__ になるの __ではなく__、先程の実行結果のように、__どの項目__ も __選択されない__ という __状態__ に __なります__。

:::note warn
__Dropdown__ では、__項目の値__ に __`None`__ を __設定するべきではない__。
:::

### 問題の解決方法

__この問題__ を __修正__ する __方法__ の __一つ__ に、__`play`__ メソッドの __仮引数 `ai`__ の要素が __人間の手番__ を __表す値__ を __`None` 以外__ のデータに __変更する__ という方法がありますが、__その方法__ で __`play`__ メソッドを __修正__ すると、__これまで__ に __`play`__ メソッドを __利用するプログラム__ が __動作しなくなる__ という、__互換性__ の __問題が発生__ します。

__`play`__ メソッドの __互換性を保つ__ 別の __解決方法__ として、__人間を表す__ Dropdown の __項目の値__ を __`None` 以外の値__ に __変更__ するという __方法__ があります。__`None` 以外__ の __値__ であれば __何でも構わない__ ので、本記事では __"人間"__ という __文字列__ で __表現__ することにします。

:::note
__他の候補__ として、__AI ではない__ という __意図__ の __`False`__ や、__何もない__ という __意図__ の __`0`__ や __`""`__ などが挙げられるでしょう。変更したい人は自由に変更して下さい。
:::

### `play` メソッドの修正

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

* __9、12 行目__：__`value`__ に __それぞれ__ の場合の __適切__ な __項目の値__ を __代入__ する
* __14、15 行目__：__`value`__ を使って __`ai_list`__ と __`ai_values`__ の __要素__ を __追加__ する

```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          # ai[i] が ai_values に登録済かどうかを判定する
 5          if ai[i] not in ai_values:
 6              # ラベルと項目の値を計算する
 7              if ai[i] is None:
 8                  label = "人間"
 9                  value = "人間"
10              else:
11                  label = ai[i].__name__        
12                  value = ai[i]
13              # 項目を登録する
14              ai_list.append((label, value))
15              ai_values.append(value)
元と同じなので省略
16
17  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]
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        # ai[i] が ai_values に登録済かどうかを判定する
        if ai[i] 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)
    
    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_list,
        description="〇",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=ai[0],
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_list,
        description="×",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=ai[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):
        # ai[i] が ai_values に登録済かどうかを判定する
        if ai[i] not in ai_values:
            # ラベルと項目の値を計算する
            if ai[i] is None:
                label = "人間"
+               value = "人間"
            else:
                label = ai[i].__name__        
+               value = ai[i]
            # 項目を登録する
-           ai_list.append((label, ai[i]))
+           ai_list.append((label, value))
-           ai_values.append(ai[i])
+           ai_values.append(value)
元と同じなので省略

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

__上記__ の __修正後__ に、__下記__ のプログラムで __`play`__ メソッドを __実行__ すると、残念ながら __実行結果__ の __下図左__ のように、__〇__ の __Dropdown__ には相変わらず __空白が表示__ されてしまいます。このようなことが起きる原因について少し考えてみて下さい。

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

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/f639319b-4108-8ce4-a2a0-6d66c2eb34a8.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/040bc3f6-cd01-2c8a-731a-d355a2c6eb7c.png" width="200">

### 問題の原因と修正

__上記__ の __問題__ は、__人間__ を表す __項目の値__ を __変更__ したにも __関わらず__、__Dropdown__ を __作成する際__ の、__キーワード引数 `value`__ の __値__ をそれに合わせて __変更していない__ からです。

ただし、__現状のプログラム__ では、__`ai_values`__ に __登録__ した __項目の値__ を __記録していない__ ので、__Dropdown__ の __作成時__ に __その値__ を __利用__ することが __できません__。そこで、__下記__ のプログラムのように、__それぞれ__ の __手番__ の __Dropdown__ の __項目の値__ を __`select_values`__ という __list に記録__ することで、__Dropdown__ の __作成時__ に __適切な項目__ が __選択される__ ように __修正__ します。

* __3 行目__：__それぞれ__ の __手番の担当__ を表す __Dropdown__ の __項目の値__ を __記録__ する __`select_values`__ の値を __空の list__ で __初期化__ する
* __18 行目__：__それぞれ__ の __手番の担当__ を表す __項目の値__ を __`select_values`__ に __追加__ する
* __26、33 行目__：__`select_values`__ を使って、__Dropdown__ の __`value`__ 属性の __値を設定__ する

```python
 1  def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2      # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
 3      select_values = []
 4      # ai に代入されている内容を ai_list に追加する
 5      for i in range(2):
 6          # ai[i] が ai_values に登録済かどうかを判定する
 7          if ai[i] not in ai_values:
 8              # ラベルと項目の値を計算する
 9              if ai[i] is None:
10                  label = "人間"
11                  value = "人間"
12              else:
13                  label = ai[i].__name__        
14                  value = ai[i]
15              # 項目を登録する
16              ai_list.append((label, value))
17              ai_values.append(value)
18              select_values.append(value)
19    
20      # 〇 と × の Dropdown を作成する   
21      dropdown_circle = widgets.Dropdown(
22          options=ai_list,
23          description="〇",
24          layout=widgets.Layout(width="100px"),
25          style={"description_width": "20px"},
26          value=select_values[0],
27      )
28      dropdown_cross = widgets.Dropdown(
29          options=ai_list,
30          description="×",
31          layout=widgets.Layout(width="100px"),
32          style={"description_width": "20px"},
33          value=select_values[1],
34      )       
元と同じなので省略    
35
36  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):
        # ai[i] が ai_values に登録済かどうかを判定する
        if ai[i] 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)
    
    # 〇 と × の 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):      
元と同じなので省略
    # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
+   select_values = []
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        # ai[i] が ai_values に登録済かどうかを判定する
        if ai[i] 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)
    
    # 〇 と × の 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],
    )       
元と同じなので省略    

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

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

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

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/c82389a5-2da2-7887-d64b-dfae45946602.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/e9e3640e-a1fd-b071-d702-26544be87934.png" width="200">

実は、__上記__ の __`play`__ メソッドには __重大なバグ__ が __いくつかあります__。余裕がある方は、__`play`__ メソッドで __様々な対戦__ を行ってみて、どのような __バグ__ があるかを __探してみて下さい__。

# 今回の記事のまとめ

今回の記事では、__循環インポート__ の __エラーの原因__ と __修正方法__ について __説明__ しました。

また、__`play`__ メソッドの __仮引数 `ai`__ に __代入__ された値に __応じて__、__Dropdown__ の __項目__ の __自動登録__ と、__Dropdown__ の __選択項目__ を __設定__ する方法について紹介しました。

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

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

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

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

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

# 次回の記事

https://qiita.com/ysgeso/items/1dbfec99a08b8996f53c