Pythonで〇×ゲームのAIを一から作成する　その53　dict のハッシュ可能なオブジェクトへの変換と互換性

# 目次と前回の記事

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

https://qiita.com/ysgeso/items/3b8967122379079a2a82

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

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

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

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

## これまでに作成した AI

これまでに作成した AI の アルゴリズム は以下の通りです。

| ルール | アルゴリズム |
|:-:|:--|
| __[ルール1](https://qiita.com/ysgeso/items/bdbcd4c1ab1d8731df58#%E6%9C%80%E5%88%9D%E3%81%AB%E8%A6%8B%E3%81%A4%E3%81%8B%E3%81%A3%E3%81%9F%E7%A9%BA%E3%81%84%E3%81%A6%E3%81%84%E3%82%8B%E3%83%9E%E3%82%B9%E3%81%AB%E7%9D%80%E6%89%8B%E3%82%92%E8%A1%8C%E3%81%86-ai)__  | __左上から順__ に __空いているマス__ を探し、__最初に見つかったマス__ に __着手__ する |
| __[ルール2](https://qiita.com/ysgeso/items/bdbcd4c1ab1d8731df58#%E3%83%A9%E3%83%B3%E3%83%80%E3%83%A0%E3%81%AA%E3%83%9E%E3%82%B9%E3%81%AB%E7%9D%80%E6%89%8B%E3%82%92%E8%A1%8C%E3%81%86-ai)__  | __ランダム__ なマスに __着手__ する |
| __[ルール3](https://qiita.com/ysgeso/items/1e8732db6cd1b0104276#%E3%83%AB%E3%83%BC%E3%83%AB-3-%E7%9C%9F%E3%82%93%E4%B8%AD%E3%81%AE%E3%83%9E%E3%82%B9%E3%81%AB%E5%84%AA%E5%85%88%E3%81%97%E3%81%A6%E7%9D%80%E6%89%8B%E3%81%99%E3%82%8B)__  | __真ん中__ のマスに __優先的__ に __着手__ する<br>既に __埋まっていた場合__ は __ランダム__ なマスに __着手__ する | |
| __[ルール4](https://qiita.com/ysgeso/items/1e8732db6cd1b0104276#%E3%83%AB%E3%83%BC%E3%83%AB-4%E5%9B%9B%E9%9A%85%E3%81%AE%E3%83%9E%E3%82%B9%E3%81%AB%E5%84%AA%E5%85%88%E3%81%97%E3%81%A6%E7%9D%80%E6%89%8B%E3%81%99%E3%82%8B)__  | __真ん中__、__隅__ のマスの __順__ で __優先的__ に __着手__ する<br>既に __埋まっていた場合__ は __ランダム__ なマスに __着手__ する | あり |
| __[ルール5](https://qiita.com/ysgeso/items/ff28a95dff50322e6b16#%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%9D%E3%81%AE-5%E5%8B%9D%E3%81%A6%E3%82%8B%E5%A0%B4%E5%90%88%E3%81%AB%E5%8B%9D%E3%81%A4)__  | __勝てる場合__ に __勝つ__<br>そうでない場合は __ランダム__ なマスに __着手__ する | |
| __[ルール6](https://qiita.com/ysgeso/items/e08c41f93109496fb9f9#%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%9D%E3%81%AE-6%E7%9B%B8%E6%89%8B%E3%81%AE%E5%8B%9D%E5%88%A9%E3%82%92%E9%98%BB%E6%AD%A2%E3%81%99%E3%82%8B)__  | __勝てる場合__ に __勝つ__<br>そうでない場合は __相手の勝利__ を __阻止__ する<br>そうでない場合は __ランダム__ なマスに __着手__ する |
| __[ルール6改](https://qiita.com/ysgeso/items/6c0d73cc9ef4efd62d48#%E3%83%AB%E3%83%BC%E3%83%AB-6-%E6%94%B9%E3%81%AE%E5%AE%9A%E7%BE%A9)__  | __勝てる場合__ に __勝つ__<br>そうでない場合は __相手__ が __勝利できる__ 着手を __行わない__<br>そうでない場合は __ランダム__ なマスに __着手__ する |
| __[ルール7](https://qiita.com/ysgeso/items/e08c41f93109496fb9f9#%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%9D%E3%81%AE-7%E3%83%AB%E3%83%BC%E3%83%AB3-%E3%81%A8-%E3%83%AB%E3%83%BC%E3%83%AB-6-%E3%81%AE%E4%BD%B5%E7%94%A8)__  | __真ん中__ のマスに __優先的__ に __着手__ する<br>そうでない場合は __勝てる場合__ に __勝つ__<br>そうでない場合は __相手の勝利__ を __阻止__ する<br>そうでない場合は __ランダム__ なマスに __着手__ する |
| __[ルール7改](https://qiita.com/ysgeso/items/6c0d73cc9ef4efd62d48#%E8%A9%95%E4%BE%A1%E5%80%A4%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%9F-%E3%83%AB%E3%83%BC%E3%83%AB-7-%E3%81%A7%E7%9D%80%E6%89%8B%E3%82%92%E8%A1%8C%E3%81%86-ai-%E3%81%AE%E5%AE%9A%E7%BE%A9)__  | __真ん中__ のマスに __優先的__ に __着手__ する<br>そうでない場合は __勝てる場合__ に __勝つ__<br>そうでない場合は __相手__ が __勝利できる__ 着手を __行わない__<br>そうでない場合は __ランダム__ なマスに __着手__ する |
| __[ルール8](https://qiita.com/ysgeso/items/58754ba98c286f414ea4#%E3%83%AB%E3%83%BC%E3%83%AB-8-%E3%81%AE%E5%AE%9A%E7%BE%A9)__  | __真ん中__ のマスに __優先的__ に __着手__ する<br>そうでない場合は __勝てる場合__ に __勝つ__<br>そうでない場合は __相手__ が __勝利できる__ 着手を __行わない__<br>そうでない場合は、__次__ の __自分の手番__ で __勝利できる__ ように、「__自 2 敵 0 空 1__」が 1 つ以上 __存在する__ 局面になる着手を行う<br>そうでない場合は __ランダム__ なマスに __着手__ する |

基準となる __`ai2`__ との __対戦結果__（単位は %）は以下の通りです。__太字__ は __`ai2 VS ai2` よりも__ __成績が良い__ 数値を表します。__欠陥__ の列は、__アルゴリズム__ に __欠陥__ があるため、__`ai2`__ との __対戦成績__ が __良くても強い__ とは __限らない__ ことを表します。欠陥の詳細については、関数名のリンク先の説明を見て下さい。

| 関数名 | o 勝 | o 負 | o 分 | x 勝 | x 負 | x 分 | 勝 | 負 | 分 | 欠陥 |
|:-:|--:|--:|--:|--:|--:|--:|--:|--:|--:|:-:|
| __[`ai1`](https://qiita.com/ysgeso/items/bdbcd4c1ab1d8731df58#ai1-%E3%81%AE%E5%AE%9A%E7%BE%A9)__<br>__[`ai1s`](https://qiita.com/ysgeso/items/3afe5630d7e615a28ebf#ai1s-%E3%81%AE%E4%BF%AE%E6%AD%A3)__  | __78.1__ | __17.5__ | 4.4 | __44.7__ | __51.6__ | 3.8 | __61.4__ | __34.5__ | 4.1 | あり |
| __[`ai2`](https://qiita.com/ysgeso/items/bdbcd4c1ab1d8731df58#ai2-%E3%81%AE%E5%AE%9A%E7%BE%A9)__<br>__[`ai2s`](https://qiita.com/ysgeso/items/8f3d4b3d2c02ef51a70e#%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E9%96%A2%E6%95%B0%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%9F%E3%83%AB%E3%83%BC%E3%83%AB-2-%E3%81%A7%E7%9D%80%E6%89%8B%E3%82%92%E8%A1%8C%E3%81%86-ai-%E3%81%AE%E5%AE%9A%E7%BE%A9)__  | 58.7 | 28.8 | 12.6 | 29.1 | 58.6 | 12.3 | 43.9 | 43.7 | 12.5 | |
| __[`ai3`](https://qiita.com/ysgeso/items/1e8732db6cd1b0104276#ai3-%E3%81%AE%E5%AE%9F%E8%A3%85)__<br>__[`ai3s`](https://qiita.com/ysgeso/items/8f3d4b3d2c02ef51a70e#%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E9%96%A2%E6%95%B0)__  | __69.3__ | __19.2__ | 11.5 | __38.9__ | __47.6__ | 13.5 | __54.1__ | __33.4__ | 12.5 | |
| __[`ai4`](https://qiita.com/ysgeso/items/1e8732db6cd1b0104276#%E3%83%AB%E3%83%BC%E3%83%AB-4%E5%9B%9B%E9%9A%85%E3%81%AE%E3%83%9E%E3%82%B9%E3%81%AB%E5%84%AA%E5%85%88%E3%81%97%E3%81%A6%E7%9D%80%E6%89%8B%E3%81%99%E3%82%8B)__<br>__[`ai4s`](https://qiita.com/ysgeso/items/3afe5630d7e615a28ebf#ai4s-%E3%81%AE%E4%BF%AE%E6%AD%A3%E3%81%A8%E5%8B%95%E4%BD%9C%E3%81%AE%E7%A2%BA%E8%AA%8D)__  | __83.0__ | __9.5__ | 7.4 | __57.2__ | __33.0__ | 9.7 | __70.1__ | __21.3__ | 8.6 | あり |
| __[`ai5`](https://qiita.com/ysgeso/items/ff28a95dff50322e6b16#%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%9D%E3%81%AE-5%E5%8B%9D%E3%81%A6%E3%82%8B%E5%A0%B4%E5%90%88%E3%81%AB%E5%8B%9D%E3%81%A4)__<br>__[`ai5s`](https://qiita.com/ysgeso/items/6c0d73cc9ef4efd62d48#ai5s-%E3%81%AE%E5%AE%9F%E8%A3%85)__  | __81.2__ | __12.3__ | 6.5 | __51.8__ | __39.8__ | 8.4 | __66.5__ | __26.0__ | 7.4 | |
| __[`ai6`](https://qiita.com/ysgeso/items/e08c41f93109496fb9f9#%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%9D%E3%81%AE-6%E7%9B%B8%E6%89%8B%E3%81%AE%E5%8B%9D%E5%88%A9%E3%82%92%E9%98%BB%E6%AD%A2%E3%81%99%E3%82%8B)__  | __88.9__ | __2.2__ | 8.9 | __70.3__ | __6.2__ | 23.5 | __79.6__ | __4.2__ | 16.2 |
| __[`ai6s`](https://qiita.com/ysgeso/items/e08c41f93109496fb9f9#%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%9D%E3%81%AE-6%E7%9B%B8%E6%89%8B%E3%81%AE%E5%8B%9D%E5%88%A9%E3%82%92%E9%98%BB%E6%AD%A2%E3%81%99%E3%82%8B)__  |   __88.6__ | __1.9__ | 9.5 | __69.4__ | __9.1__ | 21.5 | __79.0__ | __5.5__ | 15.5 |
| __[`ai7`](https://qiita.com/ysgeso/items/e08c41f93109496fb9f9#%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%9D%E3%81%AE-7%E3%83%AB%E3%83%BC%E3%83%AB3-%E3%81%A8-%E3%83%AB%E3%83%BC%E3%83%AB-6-%E3%81%AE%E4%BD%B5%E7%94%A8)__<br>__[`ai7s`](https://qiita.com/ysgeso/items/6c0d73cc9ef4efd62d48#ai7s-%E3%81%AE%E5%AE%9A%E7%BE%A9)__ | __95.8__ | __0.2__ | 4.0 | __82.3__ | __2.4__ | 15.3 | __89.0__ | __1.3__ | 9.7 |
| __[`ai8s`](https://qiita.com/ysgeso/items/58754ba98c286f414ea4#enum_markpats-%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%9F-ai8s-%E3%81%AE%E5%AE%9A%E7%BE%A9)__  | __98.2__ | __0.1__ | 1.6 | __89.4__ | __2.5__ | 8.1 | __93.8__ | __1.3__ | 4.9 |

# 前回の記事で修正した `enum_markpats` の問題点

[前回の記事](https://qiita.com/ysgeso/items/3b8967122379079a2a82#enum_markpats-%E3%81%AE%E4%BF%AE%E6%AD%A3)の最後で、__`enum_markpats`__ の __返り値__ を __list__ から __set__ に __変更__ するために、下記のような修正を行いました。

* __4 行目__：__`markpats`__ に __代入__ する値を、__空の list__ から __空の set__ に __修正__ する
* __9、11、15、18 行目__：__`append`__ メソッドを、__`add`__ メソッドに __修正__ する

```python
 1  from marubatsu import Marubatsu
 2
 3  def enum_markpats(self):
 4      markpats = set()
 5  
 6      # 横方向と縦方向の判定
 7      for i in range(self.BOARD_SIZE):
 8          count = self.count_marks(coord=[0, i], dx=1, dy=0)
 9          markpats.add(count)
10          count = self.count_marks(coord=[i, 0], dx=0, dy=1)
11          markpats.add(count)
12      # 左上から右下方向の判定
14      count = self.count_marks(coord=[0, 0], dx=1, dy=1)
15      markpats.add(count)
16      # 右上から左下方向の判定
17      count = self.count_marks(coord=[2, 0], dx=-1, dy=1)
18      markpats.add(count)
19
20      return markpats
21
22  Marubatsu.enum_markpats = enum_markpats
```

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

```python
from marubatsu import Marubatsu

def enum_markpats(self):
    markpats = set()
 
    # 横方向と縦方向の判定
    for i in range(self.BOARD_SIZE):
        count = self.count_marks(coord=[0, i], dx=1, dy=0)
        markpats.add(count)
        count = self.count_marks(coord=[i, 0], dx=0, dy=1)
        markpats.add(count)
    # 左上から右下方向の判定
    count = self.count_marks(coord=[0, 0], dx=1, dy=1)
    markpats.add(count)
    # 右上から左下方向の判定
    count = self.count_marks(coord=[2, 0], dx=-1, dy=1)
    markpats.add(count)

    return markpats

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

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

```diff_python
from marubatsu import Marubatsu

def enum_markpats(self):
-   markpats = []
+   markpats = set()
 
    # 横方向と縦方向の判定
    for i in range(self.BOARD_SIZE):
        count = self.count_marks(coord=[0, i], dx=1, dy=0)
-       markpats.append(count)
+       markpats.add(count)
        count = self.count_marks(coord=[i, 0], dx=0, dy=1)
-       markpats.append(count)
+       markpats.add(count)
    # 左上から右下方向の判定
    count = self.count_marks(coord=[0, 0], dx=1, dy=1)
-   markpats.append(count)
+   markpats.add(count)
    # 右上から左下方向の判定
    count = self.count_marks(coord=[2, 0], dx=-1, dy=1)
-   markpats.append(count)
+   markpats.add(count)

    return markpats

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

しかし、[以前の記事](https://qiita.com/ysgeso/items/58754ba98c286f414ea4#enum_markpats-%E3%81%AE%E5%AE%9F%E8%A3%85%E3%81%AE%E7%A2%BA%E8%AA%8D)で __`enum_markpats`__ の __実装__ を __確認__ する際に記述した下記のプログラムを実行すると、__実行結果__ のような __エラーが発生__ することがわかりました。

```python
from pprint import pprint

mb = Marubatsu()

print(mb)
pprint(mb.enum_markpats())

mb.move(1, 1)
print(mb)
pprint(mb.enum_markpats())

mb.move(0, 0)
print(mb)
pprint(mb.enum_markpats())

mb.move(1, 0)
print(mb)
pprint(mb.enum_markpats())
```

実行結果

```
略
Cell In[1], line 9
      7 for i in range(self.BOARD_SIZE):
      8     count = self.count_marks(coord=[0, i], dx=1, dy=0)
----> 9     markpats.add(count)
     10     count = self.count_marks(coord=[i, 0], dx=0, dy=1)
     11     markpats.add(count)

TypeError: unhashable type: 'collections.defaultdict'
```

このエラーは、__ハッシュ可能でない__ defaultdict を __set__ の __要素に追加__ しようとしたことが __原因__ なので、__`count_marks`__ が __ハッシュ可能なオブジェクト__ を __返す__ ように __修正__ する __必要__ があります。どのように修正すれば良いかについて少し考えてみて下さい。

# dict のハッシュ可能なオブジェクトへの変換 その 1

これまでに紹介した、__複数__ の __データ__ を __扱う__ ことができる __ハッシュ可能__ な __データ型__ には __tuple__ があるので、まず __defaultdict__ を __tuple に変換__ する __方法__ を __考える__ ことにします。

__defaultdict__ は、__既定値__ に関する処理 __以外__ では __dict__ と __同じ性質__ を持ちます。今回の記事で紹介する、__defaultdict__ を __tuple に変換__ する __方法__ では、__既定値__ に __関する処理__ は __行わない__ ので、__dict__ と __defaultdict__ は、__同じ方法__ で __tuple に変換__ できます。

:::note warn
本記事では、__以後__ は __既定値__ に関する処理を __行わない__ 場面で、__dict__ と __記述__ した場合は、__defaultdict を含める__ ことにします。
:::

## `count_marks` の修正

__`count_marks`__ の __返り値__ である __defaultdict__（__以後__ は __dict__ と __表記__）は、__〇__、__×__、__空__ の __数__ を表す __3 つのデータ__ を __持ちます__。従って、その __dict__ を、それらの __3 つの値__ を __要素__ として持つ __tuple に変換__ することが __可能__ です。__変換後__ の __tuple__ の __3 つの要素__ が表すデータの __順番__ は、どのような順番でもかまいませんが、__何らかの順番__ を __決める必要__ があります。そこで、本記事では __〇__、__×__、__空__ の __順番__ で、__数えた数__ を __要素__ として持つ __tuple に変換__ することにし、下記のプログラムのように、__`count_marks`__ を __修正__ します。

* __11 行目__：__`count`__ から __`Marubatsu.CIRCLE`__、__`Marubatsu.CROSS`__、__`Marubatsu.EMPTY`__ の __順__ で __キーの値__ を __取り出し__、__それらを要素__ とする __tuple__  を __返り値__ として __返す__ ようにする

```python
 1  from collections import defaultdict
 2
 3  def count_marks(self, coord, dx, dy):     
 4      x, y = coord   
 5      count = defaultdict(int)
 6      for _ in range(self.BOARD_SIZE):
 7          count[self.board[x][y]] += 1
 8          x += dx
 9          y += dy
10
11      return count[Marubatsu.CIRCLE], count[Marubatsu.CROSS], count[Marubatsu.EMPTY]
12
13  Marubatsu.count_marks = count_marks
```

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

```python
from collections import defaultdict

def count_marks(self, coord, dx, dy):     
    x, y = coord   
    count = defaultdict(int)
    for _ in range(self.BOARD_SIZE):
        count[self.board[x][y]] += 1
        x += dx
        y += dy

    return count[Marubatsu.CIRCLE], count[Marubatsu.CROSS], count[Marubatsu.EMPTY]

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

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

```diff_python
from collections import defaultdict

def count_marks(self, coord, dx, dy):     
    x, y = coord   
    count = defaultdict(int)
    for _ in range(self.BOARD_SIZE):
        count[self.board[x][y]] += 1
        x += dx
        y += dy

-   return count
+   return count[Marubatsu.CIRCLE], count[Marubatsu.CROSS], count[Marubatsu.EMPTY]

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


### 動作の確認

改めて先程の下記のプログラムを実行すると、__エラー__ が __発生しなくなります__。また、__実行結果__ から、同じ __マークのパターン__ のデータが __重複しなくなった__ ことが __確認__ できます。

```python
mb = Marubatsu()

print(mb)
pprint(mb.enum_markpats())

mb.move(1, 1)
print(mb)
pprint(mb.enum_markpats())

mb.move(0, 0)
print(mb)
pprint(mb.enum_markpats())

mb.move(1, 0)
print(mb)
pprint(mb.enum_markpats())
```

実行結果
```
Turn o
...
...
...

{(0, 0, 3)}
Turn x
...
.O.
...

{(1, 0, 2), (0, 0, 3)}
Turn o
X..
.o.
...

{(0, 1, 2), (1, 0, 2), (1, 1, 1), (0, 0, 3)}
Turn x
xO.
.o.
...

{(0, 0, 3), (2, 0, 1), (0, 1, 2), (1, 0, 2), (1, 1, 1)}
```

## `count_marks` の問題点

初心者の方は気づいていない人が多いと思いますが、実は、上記の __`count_marks`__ の __修正__ により、__新しい問題__ が __発生__ しています。それが何かについて少し考えてみて下さい。

具体的には、下記のプログラムのように、__`ai6s` どうし__ で __対戦__ を行うと __エラーが発生__ するという問題です。

```python
from ai import ai_match, ai2, ai6s, ai7s

ai_match(ai=[ai6s, ai6s])
```

実行結果（実行結果はランダムなので下記とは異なる場合があります）

```
略
    442 for i in range(mb.BOARD_SIZE):
    443     count = mb.count_marks(coord=[0, i], dx=1, dy=0)
--> 444     if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
    445         return -1
    446     count = mb.count_marks(coord=[i, 0], dx=0, dy=1)

TypeError: tuple indices must be integers or slices, not str
```

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

* __TypeError__
データ型（type）に関するエラー
* __invalid syntax__
tuple のインデックス（indices[^1]）は、文字列ではなく（not str）、整数（integers）またはスライス（slices）でなければならない
:::

__エラーメッセージ__ から、この __エラー__ は、下記の __2 行目__ で __発生する__ ことが分かります。

```python
        count = mb.count_marks(coord=[0, i], dx=1, dy=0)
--->    if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
```

上記の __1 行目__ の処理で、__`count`__ には __`count_marks`__ の __返り値__ が __代入__ されますが、その __データ型__ は先程 __`count_marks`__ を __修正__ したため __tuple__ になります。

__2 行目__ の __`count[mb.turn]`__ の __インデックス__ に記述されている __`mb.turn`__ には、__`"o"`__ または __`"x"`__ という __文字列__ が __代入__ されています。__tuple__ の __インデックス__ は __整数__ でなければならないので、2 行目の __`count[mb.turn]`__ を __計算__ する際に、__上記__ の __エラーが発生__ します。

この __エラー__ が __発生__ するようになった __原因__ は、__`ai6s`__ が __`count_marks`__ の __返り値__ が __dict__ であることを __前提__ に __処理__ を行っているにも __関わらず__、__`count_marks`__ の __返り値__ を __dict__ から __tuple__ に __修正__ してしまった __ため__ です。

## 互換性（compatibility）とは何か

このように、__関数__ の入力、処理、出力などの __仕様を変更__ すると、__それを利用__ する __プログラム__ が __動作しなくなる__ という __問題__ が __発生__ する __場合__ が __あります__。

:::note warn
__関数__ に限らず、__モジュール__、__ソフトウェア__、__ハードウェア__ などの __もの__ を、__別のもの__ に __置き換える__ ことが __できる__ ことを、__互換性がある__（互換性を持つ）と呼ぶ。
:::

例えば、__さまざまなメーカー__ が __乾電池__ を __発売__ していますが、（単3電池など）__同じ型__ の __電池__ であれば、__メーカー__ や __製品の種類__ が __異なって__ いても __置き換える__ ことが __できます__。このように、__異なる製品__ であっても、__同じ型__ の __電池__ には __互換性__ が __あります__。

## 互換性の考え方

__互換性__ を __考える__ 際に __重要__ となるのは、対象となる __もの__ を __利用する際__ に __重要__ となる __性質__ です。例えば、__電池__ には __さまざまな性質__ がありますが、下記の表のように、電池を __電池として利用する__ 際に __重要となる性質__ と、__そうでない性質__ があります。

* __重要な性質__
    * 大きさ（形状）
    * 電圧（ボルト数）
    * 容量（どれくらい長持ちするか）
    * 値段
* __重要でない性質__
    * 重さ
    * 表面のデザイン

__互換性__ を __考える際__ には、__重要な性質のみ__ を __考慮__ し、__重要でない__ 性質は __無視__ します。

ただし、下記のような場合など、__状況によって__、__重要となる性質__ が __変わる__ 場合があるので、__互換性__ に関しては、__どのような性質__ を __対象__ としているかを __考える必要__ があります。

* __短い間__ でしか __利用__ しない場合は、__容量__ はあまり __重要ではない__
* __購入済__ の電池の中から __選択する__ よう場合は、__値段__ はあまり __重要はない__
* __見栄え__ を __考慮__ する必要がある のであれば、__デザイン__ は __重要__ になる

:::note
別の例として、__過去は重要__ であったが、__現在__ では __古すぎて重要ではない__ 性質を、__互換性__ の __対象から除外する__ ことが良くあります。

__具体例__ としては、__2000 年頃__ の __パソコン__ で __良く使われていた__ 記憶装置に、__[フロッピーディスク](https://ja.wikipedia.org/wiki/%E3%83%95%E3%83%AD%E3%83%83%E3%83%94%E3%83%BC%E3%83%87%E3%82%A3%E3%82%B9%E3%82%AF)__ がありますが、__現在発売__ されている __パソコン__ で __フロッピーディスク__ が __使えるものはない__ でしょう。フロッピーディスクの事が良くわからない人は、最近のパソコンで利用できなくなりつつある CD や DVD を思い浮かべて下さい。

従って、__2000 年頃__ は __パソコンの互換性__ を考える際に __フロッピーディスクは重要__ でしたが、__現在__ では __フロッピーディスク__ は __互換性の対象__ とは __なりません__。
:::

## 互換性の分類

__互換性__ は、いくつかに __分類__ されます。互換性の一部の __分類__ は、互換性が __対象__ とする __性質__ を [前回の記事](https://qiita.com/ysgeso/items/3b8967122379079a2a82#%E9%9B%86%E5%90%88%E3%81%A8%E3%81%AF%E4%BD%95%E3%81%8B)で紹介した __集合__ として __考える__ と __わかりやすい__ でしょう。

:::note
__互換性__ に関する __分類__ は、この後で補足しますが、__定義__ とは __異なる意味__ で __使われる場合__ があります。そのため、__互換性__ に関する __用語__ の __使われ方__ が __変だと思った場合__ は、__文脈など__ からその __意味__ を __判断する必要__ があります。
:::


具体例として、下記の性質を持つ __同じ会社__ が __発売__ した __時計__ を例に挙げて説明します。

| 製品名 | 時刻の表示 | 日付の表示 | アラーム機能 | 発売年 |
|:--|:-:|:-:|:-:|:-:|
| __時計 A__  | 〇  |   |   | 2019  |
| __時計 B__  | 〇  | 〇 |   | 2020 |
| __時計 C__  | 〇  | 〇  | 〇  | 2021  |
| __時計 D__  | 〇  |   | 〇  | 2022  |
| __時計 E__  | 〇  |   |   | 2023  |

なお、__後から発売__ された __時計 E__ は、__時計 A__ と __同じ性質__ を持ちますが、__時計 A__ とは __デザインが異なる__ 製品だと思ってください。

### 完全互換（full compatible）

__あるもの__ が、__別のもの__ が持つ __機能__ を __すべて備えている__ ことを __完全互換__ と呼びます。__互換性が対象__ とする __性質__ を __集合__ として考えると、__[包含関係](https://qiita.com/ysgeso/items/3b8967122379079a2a82#%E5%8C%85%E5%90%AB%E9%96%A2%E4%BF%82%E3%81%A8%E7%9B%B8%E5%BD%93%E9%96%A2%E4%BF%82%E7%AD%89%E3%81%97%E3%81%84%E3%81%AE%E5%88%A4%E5%AE%9A)__ を持ちます。

なお、上記の時計の例にはありませんが、__値段__ のように、__数字で表される性質__ の場合は、__同じか__、__より優れた__ 性質を表す __数字__ である場合を __完全互換__ とみなします。時計の __値段__ の場合は、__安いほう__ が __優れている__ とみなすことができるので、あるものの値段が、別のものの値段 __以下__ の場合に __完全互換__ と __みなします__。

上記の __時計の例__ の場合、__以下__ の __組み合わせ__ は __完全互換性__ を持ちます。__時計 A__ と __時計 E__ のように、__両者の性質__ が __同じ__ である場合も __完全互換__ に __含みます__。

* 「__B、C、D、E__」は __A に対して__ 完全互換性を持つ
* __C__ は __B に対して__ 完全互換性を持つ
* __C__ は __D に対して__ 完全互換性を持つ
* 「__A、B、C、D__」は __E に対して__ 完全互換性を持つ

__X__ と __Y__ を Python の __set__（集合）として __考える__ と、__X が Y に対して完全互換性を持つ__ 場合は、__`X >= Y`__ が __`True`__ になります。

### 上位互換（upper compatible）

__上位互換__ は、__完全互換__ と __似ています__ が、__両者の性質__ が __同じ__ である場合は __含みません__。これは、「__上位__」という __用語__ が、__より優れている__ という __意味__ を表すからです。

上記の __時計の例__ の場合、__以下__ の __組み合わせ__ は __上位互換性__ を持ちます。__時計 A__ と __時計 E__ のように、__両者の性質__ が __同じ__ 場合は __上位互換性__ を __持ちません__。

* 「__B、C、D__」は __A に対して__ 上位互換性を持つ
* __C__ は __B に対して__ 上位互換性を持つ
* __C__ は __D に対して__ 上位互換性を持つ
* 「__B、C、D__」は __E に対して__ 上位互換性を持つ

__X__ と __Y__ を Python の __set__（集合）として __考える__ と、__X が Y に対して上位互換性を持つ__ 場合は、__`X > Y`__ が __`True`__ になります。

:::note
実際には、__完全互換__ を __上位互換__ と __同じ意味__ で使われる場合があります。
:::

### 下位互換（lower compatible）

__下位互換__ は、__上位互換__ の __逆__ で、__あるもの__ が、__別のもの__ の __一部__ の __性質のみ__ を持つことを表します。__上位互換__ の場合と __同様__ に、__両者の性質__ が __同じ__ である場合は __含みません__。

上記の __時計の例__ の場合、__以下__ の __組み合わせ__ は __下位互換性__ を持ちます。

* 「__A、E__」は __B に対して__ 下位互換性を持つ
* 「__A、B、D、E__」は __C に対して__ 下位互換性を持つ
* 「__A、E__」は __D に対して__ 下位互換性を持つ

__時計 B__ と __時計 D__ は、__お互いが持たない機能__ を __持つ__ ので、__下位互換__ ではありません。

__X__ と __Y__ を Python の __set__（集合）として __考える__ と、__X が Y に対して下位互換性を持つ__ 場合は、__`X < Y`__ が __`True`__ になります。

### 後方互換（backward compatible）

__同じ目的__ で __作られたもの__ の中で、__後から作られたもの__ が、__それより前__ に __作られたもの__ に対して __何らか__ の __互換性がある__ ことを、__後方互換__ と呼びます。

__時計の例__ の場合、__すべての時計__ は __時間を表示__ するという __機能を持つ__ ので、下記のように、__後から作られた__ 時計は、__それより前__ に作られた時計に対して __後方互換性__ を持ちます。

* 「__B、C、D、E__」は __A に対して__ 後方互換性を持つ
* 「__C、D、E__」は __B に対して__ 後方互換性を持つ
* 「__D、E__」は __C に対して__ 後方互換性を持つ
* __E__ は __D に対して__ 後方互換性を持つ

例えば、__E__ は __D に__ 対して __上位互換性__ を __持たない__ 事からわかるように、__後方互換__ と __上位互換__ は __異なる意味__ の用語です。他の __同様の例__ として、__機能を下げる__ ことによって __値段を下げた__、__廉価版__ の製品が __後から発売__ されることも良くあります。


しかし、__後から作られた__ ものが、__前に作られた__ ものの __改良版__ である __ことが多い__ ので、__後方互換性__ と __上位互換性__ が __同じ意味__ で使われる __場合がある__ 点に __注意__ が必要です。

### 前方互換（forward compatible）

__同じ目的__ で __作られたもの__ の中で、__前に作られたもの__ が、__それより後__ に __作られたもの__ に対して __互換性がある__ ことを、__前方互換__ と呼びます。

__時計の例__ の場合、__すべての時計__ は __時間を表示__ するという __機能を持つ__ ので、下記のように、__前に作られた__ 時計は、__それより後__ に作られた時計に対して __前方互換性__ を持ちます。

* __A__ は __B に対して__ 前方互換性を持つ
* 「__A、B__」は __C に対して__ 前方互換性を持つ
* 「__A、B、C__」は __D に対して__ 前方互換性を持つ
* 「__A、B、C、D__」は __E に対して__ 前方互換性を持つ

__過去__ に作られたものが、__未来__ に作られたものの __性質__ を __すべて持つ__ ことは __ほとんどない__ ので、__一般的__ に __前方互換性__ と __完全互換性__ や __上位互換性__ は __同時__ に __満されません__。

### 非互換（incompatible）

__2 つのもの__ が __共通の性質__ を __持たない__ ことを、__非互換__ と呼びます。先ほどの __時計の例__ では __非互換性__ を持つ __製品__ の __組み合わせ__ は __ありません__。

### 個別の性質の互換性

ここまでで説明した互換性の分類は、ものがもつ __性質の全体__ を __考慮__ したものですが、__それとは別__ に、__個別の性質__ に対して __互換性__ と __非互換性__ を __考える__ 場合があります。

例えば、「__アラーム機能__」という __性質だけ__ に __注目__ した場合、__時計 C__ と __時計 D__ は __互換性__ を __持ちます__ が、__時計A__ と __時計 C__ は __非互換性__ を持ちます。

## バージョンと互換性

__変更前__ と __変更後__ の __仕様__ を __区別する__ ための __用語__ を __バージョン__（version）と呼びます。__バージョン__ を __新しくする__ ことを __バージョンアップ__ と呼びます。また、何らかの理由で、__古いバージョン__ を __利用する__ ことを __バージョンダウン__ と呼びます。

__バージョン__ は __一般的__ に __数字__ を使って __区別__ することが多く、__仕様__ を __新しくするたび__ に __数字を増やす__ のが __一般的__ です。

例えば、ソニーのプレーステーション（PlayStation。以後は PS と表記する）というゲーム機は __PS1__、__PS2__、__PS3__、__PS4__、__PS5__ という __順番__ でバージョンが付けられています。

Microsoft 社の Office というソフトのバージョンは、Office 2023 のように、__発売した年号__ でバージョンが付けられています[^2]。

:::note
__数字以外__ をバージョンにする場合もあります。例えば、Microsoft 社の Windows という OS のバージョンはかなり __変則的__ で、Windos 95 のように __発売した年__（1995年）の __下2桁__ でバージョンをつけたり、Windows XP や Windows Me などのように、__アルファベット__ でバージョンをつけたこともありました。なお、Windows 7 以降は、8、10、11 のように __数字の順番__ でバージョンをつけています。

なお、__数字の順番__ でバージョンをつける際に、__特定の数字__ が __飛ばされる__ こともあります。例えば Windows 8 の次のバージョンは Windows 10 です。
:::

### メジャーバージョンとマイナーバージョン

__バージョン__ を __.__（半角のピリオド）で __区切って__、__複数__ の __数字や記号__ を使って __付ける__ 場合があります。例えば、__Python__ の __バージョン__ は __3.11__ のように __記述__ します。また、__複数__ の __.__ を使ってで __2.1.3__ のように、__3 つ以上__ に __分ける__ こともあります。

バージョンの __最初__ の __数字や記号__ を __メジャーバージョン__、__次__ の __数字や記号__ を __マイナーバージョン__ と呼びます。__メジャーバージョン__ や __マイナーバージョン__ の __意味__ は、__場合によって異なります__ が、__一般的__ には __下記__ のような __意味__ を持ちます。

__メジャーバージョン__ は、__一般的__ に __仕様__ が __根本的に変わる__ ような場合に __変えられる__ ので、__メジャーバージョン__ が __異なる場合__ は、__一般的__ に __多くの性質__ の __互換性__ が __保たれない__ と __考えたほうが良い__ でしょう。なお、__メジャーバージョン__ に __0__ がつけられている場合は、__一般的__ に、__正式公開前__ の __未完成__ の __バージョン__ であることを表します。

__マイナーバージョン__ は、__機能の追加__ や、__大きな不具合の修正__ などが行われた場合に __変えられます__。__多くの機能__ は __互換性__ が __保たれます__ が、__一部の機能__ は __保たれない__ こともあるので、__マイナーバージョン__ が __変わった場合__ は、__更新情報__ などを見ることを __お勧めします__。

__それ以降__ のバージョンの __数字や記号__ は、__互換性__ を __保った__ ままの、__微細な変更__ が行われた際に変えられる場合が多く、あまり __気にする必要はない__ ことが __多い__ でしょう。

例えば、__Python__ の場合は __メジャーバージョン__ が __2 から 3__ に __変わった際__ に、__仕様__ が __大幅に変更__ されました。従って、__Python__ の __バージョン 2__ で作られた __プログラム__ は、__バージョン 3__ では __多くの場合__ でそのままでは __正しく動作しない__ と __考えたほうが良い__ でしょう。

:::note
__本記事__ では __`marubatsu.py`__ や __`ai.py`__ などの __モジュール__ を __毎回の記事__ で __更新__ し、__[Github](https://github.com/ysgeso/marubatsu) 内__ の __記事の番号__ と同じ __`053`__ のような名前の __フォルダ__ に __モジュール__ の __ファイルを保存__ しています。本記事では __モジュール__ に __バージョン__ をつけて __区別していません__ が、__フォルダ名__ の __番号__ を __マイナーバージョン__ と __みなす__ ことが __できます__。

なお、長くなるので説明は省略しますが、__github__ には、フォルダにわけてバージョンを管理するよりも __洗練__ された __バージョン管理__ の __機能を持ちます__。興味がある方は調べてみると良いでしょう。
:::

## プログラムにおける互換性

下記は、今回の記事の最初で説明した、__互換性__ の __定義__ を再掲したものです。

:::note warn
__関数__ に限らず、__モジュール__、__ソフトウェア__、__ハードウェア__ などの __もの__ を、__別のもの__ に __置き換える__ ことが __できる__ ことを、__互換性がある__（互換性を持つ）と呼ぶ。
:::

上記の定義から、__プログラム__ の場合は、__関数__ や __モジュール__ の __機能__ が __共通していても__、それらを __異なるバージョン__ で __置き換えた時__ に __動作しなくなる__ 場合は、__互換性がある__ とは __みなされません__。例えば、__`count_marks`__ は、今回の記事の __修正前__ と、__修正後__ で、__いずれも__ マークの数を数えるという __共通の機能を持ちますが__、__バージョンアップ__ することで __`ai8s`__ が __正しく動作しなくなる__ ので __互換性__ があるとは __みなされません__。

__プログラム__ での __互換性__ は、__モジュール__ など、__他人が作成__ した __プログラム__ を __利用する際__ に __特に重要__ となる __概念__ です。__実際__ に __Python__ そのものの __バージョン__ や、__モジュール__ の __バージョン__ が __異なる__ と、__同じプログラム__ が正しく __動作しなくなる__ 場合がある __理由__ は、__そのプログラム__ が利用している __モジュール__ に __互換性がない__ ことが __原因__ です。

### 互換性の確認方法

__モジュール__ が __バージョンアップ__ した際に、その __モジュール__ の __すべて__ の __関数の仕様__ が __変化__ することは __ほとんどありません__。__どの関数__ の __仕様が変化__ したかについては、__更新情報__ を記した __ドキュメント__ が __示される__ のが __一般的__ です。例えば、下記のリンクは __Python__ の __バージョン 3.11__ の __更新情報__ が記述されている __公式のドキュメント__ です。

https://docs.python.org/3.11/whatsnew/3.11.html

モジュールの __ドキュメント__ の __関数__ などの __仕様の説明__ の所にも、__一般的__ に __バージョン__ による __仕様の変更__ に関する __情報__ が記述されます。例えば、下記は組み込み型のクラスである、__`float`__ の __仕様を説明__ する __公式ドキュメント__ への __リンク__ ですが、その __説明の中__ に、下記のような __バージョン__ による __仕様の変更__ の情報が __記述__ されています。

「バージョン 3.7 で変更: x は位置専用引数になりました」

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

### バージョンの確認の重要性

Python に限らず、__プログラム__ や __モジュール__ を __一般に公開__ する際に、その __プログラムを作成__ した __プログラム言語__ の __バージョン__ や、__モジュール__ の __バージョン__ などを __明記__ する __場合が多い__ のは、__互換性__ が __原因__ です。本記事でも、[初回の記事](https://qiita.com/ysgeso/items/04deccdfe7ec15314c48#%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%81%AE%E9%96%8B%E7%99%BA%E7%92%B0%E5%A2%83) でそれらを __明記__ しています。

__他人が作成__ したモジュールを __バージョンアップ__ した際に、__プログラム__ が __動作しなくなった場合__ は、__互換性__ が __原因__ である __可能性が高い__ ことを覚えておいたほうが良いでしょう。

:::note
__互換性__ は、__ソフトウェア__ だけでなく、__ハードウェア__ にも __存在__ します。例えばプレーステーション（PS）というゲーム機は、__PS1__ と __PS2__ の間で __互換性があった__ ため、__PS2__ で __PS1__ のソフトを __そのまま遊ぶ__ ことが __できました__ が、__PS3__ と __PS4__ の間には __互換性がない__ ので、__PS4__ で __PS3__ のソフトをそのまま __遊ぶ__ ことは __できません__。
:::

### プログラムの後方互換性

プログラムでは、__後方互換性__ が __特に重要__ です。例えば __関数__ や __モジュール__ の __仕様を更新__ して __バージョンアップ__ した際に、新しいバージョンが __後方互換性__ かつ __完全互換性__ を持つ場合は、__古いバージョン__ を使って作られた __プログラム__ を __変更することなく__、__そのまま利用__ することが __できます__。__上記のノート__ で紹介した、__PS2__ で __PS1 のゲーム__ を __そのまま遊べる__ のは、__PS2__ が __PS1__ に対して __後方互換性__ と __完全互換性__ を持つからです。一方、今回の記事で __バージョンアップ__ した __`count_marks`__ は、__後方互換性__ を __持たない__ ため、__バージョンアップ前__ の __`count_marks`__ を __利用__ していた __`ai6s`__ を __実行__ すると __エラーが発生__ します。

__モジュール__ などの __バージョンアップ__ を行っても、__プログラム__ が __そのまま動作__ することが多いのは、__後方互換性__ と __完全互換性__ があるように __モジュール__ が __作られている__ 場合が多いからです。例えば、__Windows__ という __OS__ は、__ほとんどの機能__ に対して __後方互換性__ があるように __作られている__ ので、__古い バージョン__ Windowsで __作られた__ ソフトの __多く__ は、__新しいバージョン__ の Windows で __そのまま実行__ することが __できます__。

__一般的__ に、__バージョンアップ時__ に __互換性__ と __表記__ した場合は、__後方互換性__ と __完全互換性__ の __両方の性質__ を __持つ場合__ の事を表します。__本記事__ でも以後は、__バージョンアップ時__ の __互換性__ という __用語__ を、そのような __意味__ で __使用__ することにします。

### プログラムの前方互換性

__過去のバージョン__ で、__未来のバージョン__ の __機能__ を __利用__ できるという、__前方互換性__ の性質を __実現__ することは、__未来の予測__ が __難しい__ ため、__一般的__ には __困難__ です。また、__プログラム__ の場合は、最近ではインターネットから __簡単にバージョンアップ__ が __できるようになった__ ので、__前方互換性__ の __性質__ が __求められる__ 場面はあまり __多くありません__。そのため、__バージョンアップ時__ に __前方互換性__ を __考慮しない__ ことが良くあります。__本記事__ でも、__関数__ や __モジュール__ の __バージョンアップ時__ に __前方互換性__ は __考慮しません__。

__前方互換性__ は、例えば __ソフトウェア__ が __有料__ な場合など、__バージョンアップ__ が __簡単でない場合__ などで __必要とされます__。__前方互換性__ は、__将来__ の __バージョンアップ__ を __想定する__ ように __仕様を設計__ するなどの方法で __実現__ します。__前方互換性__ があるソフトウェアの __代表例__ として、Microsoft 社の __Office__ の製品が挙げられます。実際に __Word__ などの Office の製品は、__新しいバージョン__ で __作られた文章__ を、__古いバージョン__ で __開いて編集__ することが __可能__ です。他にも、先程例とした挙げた __Windows__ にも __前方互換性__ があり、例えば __Windows 11__ で作成したソフトを、__多くの場合__ で __Windows 10 で実行__ することができます。

なお、__前方互換性__ は、__一般的__ に __新しいバージョン__ の __一部の機能__ しか __利用__ できないという __限界__ があります。当たり前ですが、__過去のバージョン__ の __仕様を作成__ した際に、__未来のバージョン__ で __追加される機能__ を __完全に想定__ するのは __ほぼ不可能__ だからです。実際に __Office__ 製品では、__古いバージョン__ で __新しいのバージョン__ で作られた __文書__ を __開いて編集__ することは __できますが__、__新しいバージョン__ で __追加__ された __新しい機能__ を __利用__ することは __できません__。

:::note
具体例として、__Excel__ という表計算ソフトは __前方互換性__ を __持ちます__ が、__IFS__ という関数は __Excel 2016__ で __新しく追加__ された機能なので、__それ以前のバージョン__ で __この関数を利用__ した __ファイル__ を __開いた際__ に、__この関数__ は __正しく動作しません__。
:::

## 互換性の問題を解決する方法

以後は、__互換性__ を、__後方互換性__ と __完全互換性__ を持つという __意味__ で __使用__ します。

__他人が作成__ した __モジュール__ を __利用する側__ の __立場__ では、__互換性__ が __あれば__、モジュールの __バージョン__ が __変わっても__、__自分が記述__ した __プログラム__ を __変更__ する __必要がない__ ので、__互換性__ が __あったほうが便利__ です。

一方、__自分で作成__ した __モジュール__ を一般に __公開する側__ の __立場__ では、__仕様を変更__ する際に、その __前後のバージョン__ で __互換性を持つ__ のは、それほど __簡単__ な話ではないので、__互換性__ を持つことが、必ずしも __望ましい__ とは __かぎりません__。

__仕様を変更__ した際に、__互換性__ の問題を __解決する__ ための、__さまざまな方法__ があります。本記事では、そのうちの、__良く使われる__ いくつかの __方法__ について __紹介__ します。

### 仕様を変更したモジュールを利用しているプログラムを修正する

この方法は __仕様を変更__ した __モジュール__ が、__互換性__ を全く __持たない__ 方法です。

プログラムの中で、__仕様を変更__ した __モジュール__ を __利用__ する __記述の数__ が __少ない__ 場合は、__プログラム__ のほうを、__新しい仕様__ に __合わせて修正__ するという __方法__ があります。例えば、プログラムの中で __モジュール__ を __利用__ する __記述の数__ が __1 つ__ の場合などです。

__本記事__ でも、これまでに __多くの関数__ の __仕様__ を __何度も変更__ してきましたが、__ほとんどの場合__ で __その関数__ を __利用__ する __記述__ が __少なかった__ ので、__この方法で修正__ を行ってきました。

しかし、プログラムの中で __仕様を変更__ した __モジュール__ を __利用__ する __記述の数__ が __多くなる__ と、それらを __すべて修正__ するのが __大変__ になります。また、その __モジュール__ を __一般に公開__ していた場合は、その __モジュール__ を __誰がどれだけ利用__ しているかが __わからない__ ので、そのような場合は __この方法__ による __影響__ を __予測__ することは __困難__ です。

なお、この方法には、上記のような __欠点__ は __ありますが__、__互換性より__ も __仕様の修正__ のほうが __重要__ な場合や、__互換性__ を持たせることが __困難な場合__ などで、__実際__ に __行われます__。

具体的には、__バグを修正__ するために __互換性__ をどうしても __保てない場合__ があります。特に __セキュリティ__ に __関するバグ__ など、__互換性__ を __持つより__ も __バグの修正__ のほうが __重要な場合__ は、__互換性__ を __あきらめる__ ことになります。

:::note
他の例としては、__仕様が古くなった__ 場合などで、__モジュール__ や __ソフトウェア__ そのものを、一から __設計し直したほう__ が __良い場合__ などがあり、そのような更新を行う場合は、__一般的__ に __メジャーバージョン__ を __更新__ します。

あまりよい例えではないかもしれませんが、何十年も前の __古いクーラー__ を、__修理しながら__ だましだまし __使っていく__ のを __やめて__、__新しいクーラー__ を __買う__ という場合に似ています。新しいクーラーなので、__今まで__ とは __異なる使い方__ を __覚える必要__ がありますが、__性能__ や __電気代__ などの面で __得になる__ でしょう。
:::

### 別の関数として定義する

__モジュール内__ の __関数の仕様__ を __変更__ する際に、その __関数そのもの__ を __修正__ するの __ではなく__、__元の関数__ をそのまま __残したまま__、__別の名前__ の __関数__ として __定義する__ という方法があります。この方法は、__元の関数__ が __そのまま残る__ ので、__互換性__ を __持つ__ 方法です。

一方、この方法には、__下記__ のような __問題__ があります。

* __修正前__ と __修正後__ の __プログラム__ が __ほとんど同じ場合__ は、__似たような処理__ を行う __関数__ が __2 つ定義__ されてしまう
* __それぞれ__ の __関数__ に __別の名前__ を __付ける必要__ があるため、__修正__ を __何度__ も __重ねていく__ と、__同じような名前__ の __関数__ が __複数定義__ されてしまうため、__区別が大変__ になる

__`count_marks`__ に __この方法を適用__ する場合、__下記__ の __2 つ__ の __メソッドを定義__ する事になります。__2 つ目__ の __`count_marks_by_tuple`__ が __tuple__ で __マークのパターン__ を __返す__ メソッドですが、__`count_marks`__ と __異なる__ のは、__最後__ の __return 文だけ__ です。

```python
    def count_marks(self, coord, dx, dy):    
        x, y = coord   
        count = defaultdict(int)
        for _ in range(self.BOARD_SIZE):
            count[self.board[x][y]] += 1
            x += dx
            y += dy

        return count    

    def count_marks_by_tuple(self, coord, dx, dy):    
        x, y = coord   
        count = defaultdict(int)
        for _ in range(self.BOARD_SIZE):
            count[self.board[x][y]] += 1
            x += dx
            y += dy

        return count[Marubatsu.CIRCLE], count[Marubatsu.CROSS], count[Marubatsu.EMPTY] 
```

:::note
__`count_marks_by_tuple`__ の場合は、下記のプログラムのように __工夫する__ ことで、__同じような関数__ を __2 つ定義__ する __問題を解決__ できますが、このような工夫を __常に行える__ とは __限りません__。また、この方法では __名前の問題__ を __解決できません__。

* __2 行目__：__`count_marks`__ を __呼び出して__、__dict__ の __マークのパターン__ を __計算__ する
* __3 行目__：__dict__ を __tuple__ に __変換__ した値を __返す__

```python
def count_marks_by_tuple(self, coord, dx, dy):
    count = self.count_marks(self, coord, dx, dy):
    return count[Marubatsu.CIRCLE], count[Marubatsu.CROSS], count[Marubatsu.EMPTY]
```
:::

### 関数のデフォルト引数を利用して互換性を持つ

__関数の名前__ を __変更せず__ に、__仮引数__ を使って __互換性を持つ__ という方法があります。この __方法__ では、__仮引数__ に __代入された値__ によって、その関数が __修正前の処理__ を行うか、__修正後の処理__ を行うかを __変更できる__ ようにします。また、その際に、その __仮引数__ を __デフォルト引数__ とし、__デフォルト値__ に __修正前の処理__ を行う __値を設定__ することで、その __関数を利用__ していた __プログラム__ を __修正する必要__ が __なくなります__。__本記事__ では __この方法__ で __`count_marks`__ の __互換性を持つ__ ようにすることにします。

なお、__この方法__ は __実際__ に __良く使われる__ 方法ですが、__修正を重ねる__ たびに、__関数の中__ で __互換性を持つ__ ための __記述__ が __増えていく__ ことになるので、__下記__ のような __問題__ が __発生しやすく__ なります。そのため、ある程度以上 __古くなった__ 過去の __仕様__ に関する __互換性__ を __切り捨てて__ プログラムを __整理する__ ということも __良く行われます__。

* 関数の __仮引数__ が __増えた__ 結果、__使い方__ が __分かりづらくなる__
* 関数で __行われる処理__ が __複雑になる__ ため __分かりづらくなる__
* __上記__ の 2 つ が __原因__ で、__バグ__ が __発生しやすく__ なる

:::note
__モジュール__ の __ドキュメント__ などで、__非推奨__（deprecated）という __用語__ をよく見かけますが、これは __その機能__ を __互換性のため__ に __しばらく残しておく__ が、__将来のバージョン__ で __利用できなくなる予定__ であるということを表します。そのような機能は、__いつ使えなくなる__ か __わからない__ ので、なるべく __使用しないほうが良い__ でしょう。

例えば、__Python__ の __公式ドキュメント__ の組み込み関数 [`int` の説明](https://docs.python.org/ja/3/library/functions.html#int)には、下記のような、__非推奨__ に関する __記述__ があります。

「バージョン 3.11 で変更: \_\_trunc\_\_() への処理の委譲は非推奨になりました」
:::

## 互換性を持つ `count_marks` の修正

__互換性を持つ__ ように、__`count_marks`__ を __修正__ するためには、__互換性のため__ の __仮引数__ の __名前__ を __考える__ 必要があります。そこで、その __仮引数__ を __以下__ のように __定義__ する事にします。

__名前__：__返り値__ の __データ型__（datatype）を __表す__ ので、__`datatype`__ という名前にする
__意味__：__`"dict"`__ という __文字列__ の場合は __dict を返し__、__それ以外__ の場合は __tuple を返す__
__規定値__：それまでに __dict が返される__ と __想定__ してこの関数を利用していた __プログラム__ を __修正しなくても済む__ ように、__`"dict"` を規定値__ とする

:::note
上記以外にも、__仮引数__ を __下記__ のように __定義__ する方法も __考えられます__。__もっと良い__ 仮引数の定義の方法を __思いついた人__ は __その方法__ を __採用__ しても __かまいません__。

* __実際__ に __返される__ 値の __データ型__ が __defaultdict__ であることを考慮して、__`"dict"`__ または __`"defaultdict"`__ が __代入__ された __場合__ に __dict を返す__ ようにする
* 文字列 __ではなく__、__`dict`__ という __組み込み型のクラス__ そのものが __代入__ された __場合__ に、__dict を返す__ ようにする
* __仮引数__ の __名前__ を例えば、__`return_by_dict`__ のようにし、__`True`__ が __代入__ された __場合__ に __dict__ を、__`False`__ が __代入__ された __場合__ に __tuple を返す__ ようにする
:::

下記のプログラムは、__仮引数__ に __`datatype`__ を __追加__ した __`count_marks`__ の __定義__ です。

* __1 行目__：__デフォルト値__ を __`"dict"`__ とする __デフォルト引数 `datatype`__ を __追加__ する
* __9、10 行目__：__`datatype`__ が __`"dict"`__ の場合に、__dict が代入__ された __`count`__ をそのまま __返す__
* __11、12 行目__：__そうでない__ 場合に、__`count`__ を __tuple に変換__ した値を __返す__

```python
 1  def count_marks(self, coord, dx, dy, datatype="dict"):     
 2      x, y = coord   
 3      count = defaultdict(int)
 4      for _ in range(self.BOARD_SIZE):
 5          count[self.board[x][y]] += 1
 6          x += dx
 7          y += dy
 8
 9      if datatype == "dict":
10          return count
11      else:
12          return count[Marubatsu.CIRCLE], count[Marubatsu.CROSS], count[Marubatsu.EMPTY]
13
14  Marubatsu.count_marks = count_marks
```

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

```python
def count_marks(self, coord, dx, dy, datatype="dict"):     
    x, y = coord   
    count = defaultdict(int)
    for _ in range(self.BOARD_SIZE):
        count[self.board[x][y]] += 1
        x += dx
        y += dy

    if datatype == "dict":
        return count
    else:
        return count[Marubatsu.CIRCLE], count[Marubatsu.CROSS], count[Marubatsu.EMPTY]

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

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

```diff_python
def count_marks(self, coord, dx, dy, datatype="dict"):     
    x, y = coord   
    count = defaultdict(int)
    for _ in range(self.BOARD_SIZE):
        count[self.board[x][y]] += 1
        x += dx
        y += dy

-   return count[Marubatsu.CIRCLE], count[Marubatsu.CROSS], count[Marubatsu.EMPTY]
+   if datatype == "dict":
+       return count
+   else:
+       return count[Marubatsu.CIRCLE], count[Marubatsu.CROSS], count[Marubatsu.EMPTY]

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


:::note
__上記__ の __`count_marks`__ は、__`datatype`__ に __`"dict"` 以外__ のデータが __代入__ されている場合に __tuple__  が __返されます__。そのため、例えば __`datatype`__ に __`"int"`__ という __文字列__ が __代入__ されている場合でも、__整数型ではなく__、__tuple__ が __返されます__。その点が __気になる__ 方は、__最後__ の __if 文__ を __下記__ のプログラムのように __修正__ して下さい。

ただし、下記のように __厳密な処理__ を __記述しなくても__、__互換性を持つ__ という __目的__ を __達成できる__ ので、__元のプログラム__ の ままでも __問題はない__ でしょう。__本記事__ でも、下記のプログラムのようには __修正しない__ ことにします。

* __3、4 行目__：__`datatype`__ が __`"tuple"`__ の __場合__ に __tuple を返す__ ようにする
* __5 ～ 7 行目__：__`datatype`__ が __`"dict"` でも__、__`"tuple"` でもない__ 場合は「不正（invalid）なデータ型」という意味の __エラーメッセージを表示__ して __`None` を返す__

```python
略
1  if datatype == "dict":
2      return count
3  elif datatype == "tuple":
4      return count[Marubatsu.CIRCLE], count[Marubatsu.CROSS], count[Marubatsu.EMPTY]
5  else:
6      print("invalid datatype", datatype)
7      return None
```
:::

## `count_marks` の互換性の確認

__`count_marks`__ が __互換性を持つ__ ようになったので、今度は下記のプログラムを実行しても __エラー__ が __発生しなくなり__、__実行結果__ から __`ai6s`__ が __正しく動作__ することも __確認__ できます。

```python
ai_match(ai=[ai6s, ai6s])
```

実行結果（実行結果はランダムなので下記とは異なる場合があります）

```
ai6s VS ai6s
count     win    lose    draw
o        3060    1726    5214
x        1761    3100    5139
total    4821    4826   10353

ratio     win    lose    draw
o       30.6%   17.3%   52.1%
x       17.6%   31.0%   51.4%
total   24.1%   24.1%   51.8%
```

## `enum_markpats` の修正と確認

__`count_marks`__ の __仕様に合わせて__、__`enum_markpats`__ を下記のように __修正__ します。

* __6、8、11、14 行目__：__`count_marks`__ に __キーワード引数 `datatype="tuple"`__ を __記述__ する[^3]

```python
 1  def enum_markpats(self):
 2      markpats = set()
 3
 4      # 横方向と縦方向の判定
 5      for i in range(self.BOARD_SIZE):
 6          count = self.count_marks(coord=[0, i], dx=1, dy=0, datatype="tuple")
 7          markpats.add(count)
 8          count = self.count_marks(coord=[i, 0], dx=0, dy=1, datatype="tuple")
 9          markpats.add(count)
10      # 左上から右下方向の判定
11      count = self.count_marks(coord=[0, 0], dx=1, dy=1, datatype="tuple")
12      markpats.add(count)
13      # 右上から左下方向の判定
14      count = self.count_marks(coord=[2, 0], dx=-1, dy=1, datatype="tuple")
15      markpats.add(count)
16
17      return markpats
18
19  Marubatsu.enum_markpats = enum_markpats
```

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

```python
def enum_markpats(self):
    markpats = set()
 
    # 横方向と縦方向の判定
    for i in range(self.BOARD_SIZE):
        count = self.count_marks(coord=[0, i], dx=1, dy=0, datatype="tuple")
        markpats.add(count)
        count = self.count_marks(coord=[i, 0], dx=0, dy=1, datatype="tuple")
        markpats.add(count)
    # 左上から右下方向の判定
    count = self.count_marks(coord=[0, 0], dx=1, dy=1, datatype="tuple")
    markpats.add(count)
    # 右上から左下方向の判定
    count = self.count_marks(coord=[2, 0], dx=-1, dy=1, datatype="tuple")
    markpats.add(count)

    return markpats

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

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

```diff_python
def enum_markpats(self):
    markpats = set()
 
    # 横方向と縦方向の判定
    for i in range(self.BOARD_SIZE):
-       count = self.count_marks(coord=[0, i], dx=1, dy=0)
+       count = self.count_marks(coord=[0, i], dx=1, dy=0, datatype="tuple")
        markpats.add(count)
-       count = self.count_marks(coord=[i, 0], dx=0, dy=1)
+       count = self.count_marks(coord=[i, 0], dx=0, dy=1, datatype="tuple")
        markpats.add(count)
    # 左上から右下方向の判定
-   count = self.count_marks(coord=[0, 0], dx=1, dy=1)
+   count = self.count_marks(coord=[0, 0], dx=1, dy=1, datatype="tuple")
    markpats.add(count)
    # 右上から左下方向の判定
-   count = self.count_marks(coord=[2, 0], dx=-1, dy=1)
+   count = self.count_marks(coord=[2, 0], dx=-1, dy=1, datatype="tuple")
    markpats.add(count)

    return markpats

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


次に、__修正__ した __`enum_markpats`__ が __正しく動作するか__ を、先程のプログラムで __確認__ します。実行結果は先ほどと同じなので省略しますが、__正しく動作__ することが __確認__ できます。

```python
mb = Marubatsu()

print(mb)
pprint(mb.enum_markpats())

mb.move(1, 1)
print(mb)
pprint(mb.enum_markpats())

mb.move(0, 0)
print(mb)
pprint(mb.enum_markpats())

mb.move(1, 0)
print(mb)
pprint(mb.enum_markpats())
```

## `ai8s` の修正方法 その 1

__`enum_markpats`__ が __返す値__ が、__dict を要素__ とする __list__ から、__tuple を要素__ とする __list__ に __変更__ されたので、それに合わせて __`ai8s`__ の下記のプログラムの __5、8 行目__ の __条件式__ の部分を __修正__ する __必要__ があります。どのように修正すれば良いかについて少し考えてみて下さい。

```python
 1  def ai8s(mb, debug=False):
 2      def eval_func(mb):
略
 3          markpats = mb.enum_markpats()
 4          # 相手が勝利できる場合は評価値として -1 を返す
 5          if {mb.turn: 2, Marubatsu.EMPTY: 1} in markpats:
 6              return -1
 7          # 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
 8          elif {mb.last_turn: 2, Marubatsu.EMPTY: 1} in markpats:
 9              return 1
10          # それ以外の場合は評価値として 0 を返す
11          else:
12              return 0
略
```

まず、2 つある条件式のうち、__5 行目__ の下記の __条件式__ を __修正__ する __方法__ を __考えます__。

```python
if {mb.turn: 2, Marubatsu.EMPTY: 1} in markpats:
    return -1
```

上記は、「__自 0 敵 2 空 1__」という、敵が勝利できる __マークのパターン__ を表す __dict__ が __`markpats`__ の __要素に存在するか__ どうかを __判定__ しています。従って、これを、__そのマークのパターン__ を表す __tuple に変更__ する必要があります。

__`enum_markpats`__ が __返す__、__マークのパターン__ の __一覧__ の中の __tuple__ の要素には、「__〇__、__×__、__空__」の __順__ でマークの数が代入されます。従って、「__自 0 敵 2 空__ 1」を表す __tuple__ は、__自分の手番__ の __種類によって__、下記のように __異なります__。

* __自分の手番__ が __〇__ の場合は、「__自 0 敵 2 空 1__」は、「__〇 0 × 2 空 1__」なので、 __このマークのパターン__ を表す __tuple__ は __`(0, 2, 1)`__ である

* __自分の手番__ が __×__ の場合は、「__自 0 敵 2 空 1__」は、「__〇 2 × 0 空 1__」なので、__このマークのパターン__ を表す __tuple__ は __`(2, 0, 1)`__ である

下記は、__自分の手番__ が __〇__ と __×__ の __場合に分けて__ 上記の __条件式を修正__ したプログラムです。なお、__`mb`__ は __相手の手番__ の __局面__ なので、__自分の手番__ が __〇 であるか__ どうかは、__`mb.turn == Marubatsu.CROSS`__ で __判定__ する __必要がある__ 点に __注意__ して下さい。

```python
        # 相手が勝利できる場合は評価値として -1 を返す
        if mb.turn == Marubatsu.CROSS and (0, 2, 1) in markpats:
            return -1
        if mb.turn == Marubatsu.CIRCLE and (2, 0, 1) in markpats:
            return -1
```

また、__or 演算子__ を __利用__ することで、下記のプログラムのように __1 つの if 文__ に __まとめる__ こともできます。__or 演算子__ のほうが、__and 演算子より__ も __優先順位__ が __低い__ ので、__1 つの if 文__ に __まとめる際__ に、__元の条件式__ を __`()` で囲う必要__ がある点に __注意__ が必要です。

```python
        # 相手が勝利できる場合は評価値として -1 を返す
        if (mb.turn == Marubatsu.CROSS and (0, 2, 1) in markpats) or \
           (mb.turn == Marubatsu.CIRCLE and (2, 0, 1) in markpats):
            return -1
```

### `ai8s` の修正

__もう一つ__ の __条件式__ も __同様の方法__ で __記述__ できるので、__`ai8s`__ を下記のプログラムのように __修正__ します。

```python
from ai import ai_by_score

def ai8s(mb, debug=False):
    def eval_func(mb):
        # 真ん中のマスに着手している場合は、評価値として 2 を返す
        if mb.last_move == (1, 1):
            return 3
    
        # 自分が勝利している場合は、評価値として 1 を返す
        if mb.status == mb.last_turn:
            return 2

        markpats = mb.enum_markpats()
        # 相手が勝利できる場合は評価値として -1 を返す
        if (mb.turn == Marubatsu.CROSS and (0, 2, 1) in markpats) or \
           (mb.turn == Marubatsu.CIRCLE and (2, 0, 1) in markpats):
            return -1
        # 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
        elif (mb.turn == Marubatsu.CROSS and (2, 0, 1) in markpats) or \
           (mb.turn == Marubatsu.CIRCLE and (0, 2, 1) in markpats):
            return 1
        # それ以外の場合は評価値として 0 を返す
        else:
            return 0

    return ai_by_score(mb, eval_func, debug=debug) 
```

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

```diff_python
from ai import ai_by_score

def ai8s(mb, debug=False):
    def eval_func(mb):
        # 真ん中のマスに着手している場合は、評価値として 2 を返す
        if mb.last_move == (1, 1):
            return 3
    
        # 自分が勝利している場合は、評価値として 1 を返す
        if mb.status == mb.last_turn:
            return 2

        markpats = mb.enum_markpats()
        # 相手が勝利できる場合は評価値として -1 を返す
-       if {mb.turn: 2, Marubatsu.EMPTY: 1} in markpats:        
+       if (mb.turn == Marubatsu.CROSS and (0, 2, 1) in markpats) or \
+          (mb.turn == Marubatsu.CIRCLE and (2, 0, 1) in markpats):
            return -1
        # 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
-       elif {mb.last_turn: 2, Marubatsu.EMPTY: 1} in markpats:
+       elif (mb.turn == Marubatsu.CROSS and (2, 0, 1) in markpats) or \
+          (mb.turn == Marubatsu.CIRCLE and (0, 2, 1) in markpats):
            return 1
        # それ以外の場合は評価値として 0 を返す
        else:
            return 0

    return ai_by_score(mb, eval_func, debug=debug) 
```
</details>

### 動作の確認

[前回の記事](https://qiita.com/ysgeso/items/58754ba98c286f414ea4#enum_markpats-%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%9F-ai8s-%E3%81%AE%E5%AE%9A%E7%BE%A9)で、__`enum_markpats`__ を __実装__ した際に __`ai8s2`__ という、__`ai8s`__ とは __異なる名前__ で __ルール 8__ の __AI を定義__ したので、__今回__ は何故 __そのようにしないのか__ と __思った人がいるかも__ しれません。前回の記事で __`ai8s`__ と __異なる名前__ で __AI を定義__ したのは、__`enum_markpats`__ を __利用するように修正__ した __AI__  が __正しく動作するか__ どうかを、__`enum_markpats`__ を __利用しない修正前__ の __`ai8s`__ と __対戦して確認__ できるように __するため__ でした。一方、__今回の場合__ は、__修正前__ の __`ai8s`__ も __`enum_markpats`__ を __利用しています__。そのため、元の __`ai8s`__ そのものが __動作しなく__ なっており、__`ai8s`__ を __残しておいても意味がない__ からです。従って、__修正した `ai8s`__ が __正しく動作するか__ どうかは、__別の方法__ で __確認__ する __必要__ があります。

その方法は、__`ai8s` VS `ai2`__ と __`ai8s` VS `ai7s`__ の対戦を行い、__[以前の記事](https://qiita.com/ysgeso/items/58754ba98c286f414ea4#%E4%BF%AE%E6%AD%A3%E3%81%AE%E7%A2%BA%E8%AA%8D)__ で行った __同じ組み合わせ__ の __対戦結果__ と __比較__ して __成績__ が __ほぼ変わらない__ ことを __確認する__ という方法です。

下記は __`ai2`__ との対戦です。

```python
ai_match(ai=[ai8s, ai2])
```

実行結果（実行結果はランダムなので下記とは異なる場合があります）

```
ai8s VS ai2
count     win    lose    draw
o        9847      10     143
x        8934     246     820
total   18781     256     963

ratio     win    lose    draw
o       98.5%    0.1%    1.4%
x       89.3%    2.5%    8.2%
total   93.9%    1.3%    4.8%
```

下記は __`ai7s`__ との対戦です。

```python
ai_match(ai=[ai8s, ai7s])
```

実行結果（実行結果はランダムなので下記とは異なる場合があります）

```
ai8s VS ai7s
count     win    lose    draw
o        3365     384    6251
x         378    2871    6751
total    3743    3255   13002

ratio     win    lose    draw
o       33.7%    3.8%   62.5%
x        3.8%   28.7%   67.5%
total   18.7%   16.3%   65.0%
```

下記は、__前回__ と __今回__ の __対戦結果__ の __表__ です。いずれも前回と今回で __成績__ は __ほぼ変わらない__ ので、__正しく修正__ された __可能性が高い__ ことが __確認__ できました。

| 関数名 | o 勝 | o 負 | o 分 | x 勝 | x 負 | x 分 | 勝 | 負 | 分 |
|:-:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
| 前回の __VS `ai2`__  | 98.2 | 0.1 | 1.6 | 89.4 | 2.5 | 8.1 | 93.8 | 1.3 | 4.9 |
| 今回の __VS `ai2`__  | 98.5 | 0.1 | 1.4 | 89.3 | 2.5 | 8.2 | 93.9 | 1.3 | 4.8 |
| 前回の __VS `ai7s`__  | 33.4 | 3.9 | 62.6 | 3.9 | 28.8 | 67.3 | 18.7 | 16.4 | 65.0 |
| 今回の __VS `ai7s`__  | 33.7 | 3.8 | 62.5 | 3.8 | 28.7 | 67.5 | 18.7 | 16.3 | 65.8 |

## `ai8s` の修正方法 その 2

__先程__ の __`ai8s`__ は、__自分の手番__ によって __異なる tuple__ で __判定を行う必要__ があるため、__プログラム__ が __長く__、__分かりづらくなる__ という __欠点__ があります。その __原因__ は、__`count_marks`__ が返す __tuple__ の __要素__ が「__〇 × 空__」のように __マークを基準__ に __数えている__ のに対し、__判定__ を __行う際__ の __マークのパターン__ では「__自 敵 空__」のように、マークではなく、__誰の手番__ であるかを __基準__ に __数えている__ からです。そのため、この欠点は、__`count_marks`__ が返す __tuple の要素__ を、__誰の手番__ であるかを __基準__ に __数える__ ように __修正__ することで __解消__ できます。

### `count_marks` の修正

__`count_marks`__ を下記のプログラムのように __修正__ します。

* __12 行目__：__直前の手番__ のマーク、__現在の手番__ のマーク、__空のマス__ の __順__ で __数えた数__ を __要素__ として持つ __tuple を返す__ ように __修正__ する

:::note
「__直前の手番__ のマーク、__現在の手番__ のマーク、__空のマス__」の __順番__ は、__上記以外__ の順番にしても __かまいません__ が、その場合はこの後で __`ai8s`__ のプログラムを、__その順番__ に __合わせて修正__ する __必要__ があります。
:::

```python
 1  def count_marks(self, coord, dx, dy, datatype="dict"):     
 2      x, y = coord   
 3      count = defaultdict(int)
 4      for _ in range(self.BOARD_SIZE):
 5          count[self.board[x][y]] += 1
 6          x += dx
 7          y += dy
 8
 9      if datatype == "dict":
10          return count
11      else:
12          return count[self.last_turn], count[self.turn], count[Marubatsu.EMPTY]
13
14  Marubatsu.count_marks = count_marks
```

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

```python
def count_marks(self, coord, dx, dy, datatype="dict"):     
    x, y = coord   
    count = defaultdict(int)
    for _ in range(self.BOARD_SIZE):
        count[self.board[x][y]] += 1
        x += dx
        y += dy

    if datatype == "dict":
        return count
    else:
        return count[self.last_turn], count[self.turn], count[Marubatsu.EMPTY]

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

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

```diff_python
def count_marks(self, coord, dx, dy, datatype="dict"):     
    x, y = coord   
    count = defaultdict(int)
    for _ in range(self.BOARD_SIZE):
        count[self.board[x][y]] += 1
        x += dx
        y += dy

    if datatype == defaultdict:
        return count
    else:
-       return count[Marubatsu.CIRCLE], count[Marubatsu.CROSS], count[Marubatsu.EMPTY]
+       return count[self.last_turn], count[self.turn], count[Marubatsu.EMPTY]


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


### `ai8s` の修正

__上記の修正__ を行うことで、「__自 x 敵 y 空 z__」という __マークのパターン__ を、__常に `(x, y, z)`__ という __tuple__ で __表現__ できるようになります。従って、__`ai8s`__ は、下記のプログラムのように、__手番__ に __関わらず__、__同じ条件式__ で __簡潔に記述__ できます。なお、__`mb`__ は __相手の手番__ の __局面__ なので、__自分の手番__ は、__`mb`__ の __直前の手番__ で表される点に __注意__ して下さい。

* 13 行目：「__自 0 敵 2 空 1__」は、常に __`(0, 2, 1)`__ という __tuple で表現__ できる
* 16 行目：「__自 2 敵 0 空 1__」は、常に __`(2, 0, 1)`__ という __tuple で表現__ できる

```python
 1  def ai8s(mb, debug=False):
 2      def eval_func(mb):
 3          # 真ん中のマスに着手している場合は、評価値として 2 を返す
 4          if mb.last_move == (1, 1):
 5              return 3
 6   
 7          # 自分が勝利している場合は、評価値として 1 を返す
 8          if mb.status == mb.last_turn:
 9              return 2
10
11          markpats = mb.enum_markpats()
12          # 相手が勝利できる場合は評価値として -1 を返す
13          if (0, 2, 1) in markpats:
14              return -1
15          # 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
16          elif (2, 0, 1) in markpats:
17              return 1
18          # それ以外の場合は評価値として 0 を返す
19          else:
20              return 0
21
22      return ai_by_score(mb, eval_func, debug=debug) 
```

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

```python
def ai8s(mb, debug=False):
    def eval_func(mb):
        # 真ん中のマスに着手している場合は、評価値として 2 を返す
        if mb.last_move == (1, 1):
            return 3
    
        # 自分が勝利している場合は、評価値として 1 を返す
        if mb.status == mb.last_turn:
            return 2

        markpats = mb.enum_markpats()
        # 相手が勝利できる場合は評価値として -1 を返す
        if (0, 2, 1) in markpats:
            return -1
        # 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
        elif (2, 0, 1) in markpats:
            return 1
        # それ以外の場合は評価値として 0 を返す
        else:
            return 0

    return ai_by_score(mb, eval_func, debug=debug) 
```
</details>

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

```diff_python
def ai8s(mb, debug=False):
    def eval_func(mb):
        # 真ん中のマスに着手している場合は、評価値として 2 を返す
        if mb.last_move == (1, 1):
            return 3
    
        # 自分が勝利している場合は、評価値として 1 を返す
        if mb.status == mb.last_turn:
            return 2

        markpats = mb.enum_markpats()
        # 相手が勝利できる場合は評価値として -1 を返す
-       if (mb.turn == Marubatsu.CROSS and (0, 2, 1) in markpats) or \
-          (mb.turn == Marubatsu.CIRCLE and (2, 0, 1) in markpats):
+       if (0, 2, 1) in markpats:
            return -1
        # 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
-       elif (mb.turn == Marubatsu.CROSS and (2, 0, 1) in markpats) or \
-          (mb.turn == Marubatsu.CIRCLE and (0, 2, 1) in markpats):
+       elif (2, 0, 1) in markpats:
            return 1
        # それ以外の場合は評価値として 0 を返す
        else:
            return 0

    return ai_by_score(mb, eval_func, debug=debug) 
```
</details>

### 動作の確認

__先程と同様__ に、__`ai2`__、__`ai7s`__ と __対戦__ することで、__正しく修正されているか__ どうかを __確認__ します。まず、__`ai2`__ と __対戦__ します。

```python
ai_match(ai=[ai8s, ai2])
```

実行結果（実行結果はランダムなので下記とは異なる場合があります）

```
ai8s VS ai2
count     win    lose    draw
o        9832      11     157
x        8895     245     860
total   18727     256    1017

ratio     win    lose    draw
o       98.3%    0.1%    1.6%
x       88.9%    2.5%    8.6%
total   93.6%    1.3%    5.1%
```

次に、__`ai7s`__ と __対戦__ します。

```python
ai_match(ai=[ai8s, ai7s])
```

実行結果（実行結果はランダムなので下記とは異なる場合があります）

```
ai8s VS ai7s
count     win    lose    draw
o        3355     433    6212
x         366    2904    6730
total    3721    3337   12942

ratio     win    lose    draw
o       33.6%    4.3%   62.1%
x        3.7%   29.0%   67.3%
total   18.6%   16.7%   64.7%
```

下記は、__前回__ と __今回__ の __対戦結果__ の __表__ です。いずれも前回と今回で __成績__ は __ほぼ変わらない__ ので、__正しく修正__ された __可能性が高い__ ことが __確認__ できました。

| 関数名 | o 勝 | o 負 | o 分 | x 勝 | x 負 | x 分 | 勝 | 負 | 分 |
|:-:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
| 前回の __VS `ai2`__  | 98.2 | 0.1 | 1.6 | 89.4 | 2.5 | 8.1 | 93.8 | 1.3 | 4.9 |
| 今回の __VS `ai2`__  | 98.3 | 0.1 | 1.6 | 88.9 | 2.5 | 8.6 | 93.6 | 1.3 | 5.1 |
| 前回の __VS `ai7s`__  | 33.4 | 3.9 | 62.6 | 3.9 | 28.8 | 67.3 | 18.7 | 16.4 | 65.0 |
| 今回の __VS `ai7s`__  | 33.6 | 4.3 | 62.1 | 3.7 | 29.0 | 67.3 | 18.6 | 16.7 | 64.7 |

# `ai8s` の処理速度の検証

[前回の記事](https://qiita.com/ysgeso/items/3b8967122379079a2a82#%E8%A6%81%E7%B4%A0%E3%81%AE%E6%95%B0%E3%81%A8%E5%87%A6%E7%90%86%E9%80%9F%E5%BA%A6%E3%81%AE%E5%B7%AE%E3%81%AE%E9%96%A2%E4%BF%82)で __list__ を使って __`ai8s`__ を __実装__ した場合と、__set__ を使って __実装__ した場合の __処理時間__ を __比較__ すると説明しました。先程 __set__ で __`ai8s`__ を __実装__ したので、実際に __比較__ します。

__筆者のパソコン__ では、下記のプログラムで __`ai8s` どうし__ を __対戦__ すると __約 47.2 秒__ かかりました。[以前の記事](https://qiita.com/ysgeso/items/58754ba98c286f414ea4#ai8s-%E3%81%A8-ai8s2-%E3%81%AE%E5%87%A6%E7%90%86%E9%80%9F%E5%BA%A6%E3%81%AE%E6%AF%94%E8%BC%83)で __list__ を使って実装した __`ai8s2` どうし__ の対戦では、__約 45.5 秒__ かかったので、__list__ と __set__ で __ほとんど処理速度__ が __変わらない__ ことが分かります。

```python
ai_match(ai=[ai8s, ai8s])
```

__処理速度__ がほとんど __変わらない原因__ は、__`ai8s`__ では、__list__ や __set__ の __要素の数__ が __最大で 8__ であり、[前回の記事](https://qiita.com/ysgeso/items/3b8967122379079a2a82#%E8%A6%81%E7%B4%A0%E3%81%AE%E6%95%B0%E3%81%A8%E5%87%A6%E7%90%86%E9%80%9F%E5%BA%A6%E3%81%AE%E5%B7%AE%E3%81%AE%E9%96%A2%E4%BF%82)で説明したように、__list__ や __set__ の __要素の数__ が __10 前後__ のような __少ない場合__ は、__処理速度__ に __差__ が __ほとんどない__ からだと思われます。



# 今回の記事のまとめ

今回の記事では、__dict__ を __ハッシュ可能なオブジェクト__ に __変換する方法__ として、__tuple に変換__ する方法について説明しました。

また、__互換性__ という __概念を説明__ し、__関数__ や __モジュール__ の __仕様を修正__ した際の、__互換性の問題__ への __いくつかの対処法__ について __説明__ しました。

次回の記事では、__dict__ を __tuple 以外__ の __ハッシュ可能なオブジェクト__ に __変換する方法__ などについて説明します。

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

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

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

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

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

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

https://github.com/ysgeso/marubatsu/blob/master/053/ai_new.py

# 次回の記事

https://qiita.com/ysgeso/items/085fd15fbd91d5cdf770

[^1]:__indices__ は、__index__（インデックス）の __複数形__ です
[^2]:Office 365 という、クラウドの Office 製品は __例外__ です。クラウドと 365 の意味を説明すると長くなるので省略しますが、興味がある方は調べてみると良いでしょう
[^3]:実際には、__`"dict"` 以外__ であれば、キーワード引数の __`datatype`__ に代入する値に __何を記述しても構いません__ が、__分かりやすさ__ を __重視__ して __`"tuple"` を記述__ しました