Pythonで〇×ゲームのAIを一から作成する　その54　namedtuple を使った dict の変換

# 目次と前回の記事

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

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

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

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

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

https://github.com/ysgeso/marubatsu/blob/master/054/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 |

# 修正した `count_marks` の問題点

[前回の記事](https://qiita.com/ysgeso/items/af8b5dda0ddffd5c6d13#count_marks-%E3%81%AE%E4%BF%AE%E6%AD%A3-1)で修正した、__`count_marks`__ が __返り値__ として返す __tuple__ の __要素__ の __意味__ は、__順番__ に「__直前の手番__ のマーク、__現在の手番__ のマーク、__空のマス__」の数のように決めましたが、__tuple__ は __dict のよう__ に、それぞれの __要素の意味__ を __表現__ できる __キー__ を __持たない__ ので、例えば __`(2, 0, 1)`__ という __tuple__ を __見ただけ__ では、それぞれの __要素__ が __何を意味する__ のかが __わからない__ という __欠点__ があります。

一方、__dict__ は __`{ "o": 2, "x": 0, ".": 1 }`__ のように、__キー__ を __見る__ ことで、__キーの値__ が __何を表しているか__ を __知る__ ことが __できます__ が、__dict__ は __ハッシュ可能なオブジェクトではない__ ため、__set__ の __要素に代入__ することは __できません__。

# namedtuple の性質と使い方

この __問題__ を __解決__ する方法の一つに、__tuple__ の __それぞれの要素__ に __名前がつけられた__（named）、__namedtuple__ という __ハッシュ可能__ な __データ型__ を __利用__ するという __方法__ があります。__namedtuple__ は __頻繁に利用される__ データ型では __ありません__ が、今回のように、__知っておくと便利__ な __場合がある__ ので、その使い方について説明します。

:::note warn
__namedtuple__ では、__要素__ につけられた __名前__ のことを __フィールド名__ と呼びます。
:::

ただし、__namedtuple__ には、以下のような __欠点__ もあります。そのため、__tuple__ の __要素の数が少ない__ 場合などで、__tuple__ の __それぞれの要素__ が __何を表すか__ を __容易に覚えることができる__ ような場合は、__無理__ に __namedtuple__  を __利用__ する __必要__ は __ありません__。

* __tuple__ と __異なり__、__利用の際__ に __前準備が必要__ になる
* __tuple より__ も、__データの記述__ が __多くなる__

__namedtuple__ の __詳細__ については、下記のリンク先を参照して下さい

https://docs.python.org/ja/3/library/collections.html#collections.namedtuple

## namedtuple の性質

__namedtuple__ は、以下のような、__tuple__ と __dict__ の __性質__ を __併せ持つ__ データ型です。

* __tuple の性質__ を __すべて持つ__
* __tuple の性質__ に __加えて__、__それぞれの要素__ を dict のように __名前で参照__ できる

ただし、__namedtuple__ は __以下の表の点__ で __dict__ と __性質が異なり__ ます。なお、表の　namedtuple に関する内容の詳細については、この後で詳しく説明します。

|  | dict | namedtuple |
|:--|:--|:--|
| __ミュータブルであるか__  | ミュータブル  | イミュータブル  |
| __ハッシュ可能であるか__  | ハッシュ可能でない  | ハッシュ可能である  |
| __名前に利用できるデータ__ | ハッシュ可能なオブジェクト | 文字列のみ |
| __名前による要素の参照方法__  | `[キーの名前]`  | `.フィールド名`<br>（属性の参照と同じ）  |
| __`[]` によって参照される値__ | キーの値 | インデックスの要素<br>（tuple と同じ） |
| __データの作成__ | リテラルで直接行える | サブクラスを定義し、<br>インスタンスを作成する |

このように、__namedtuple__ は、__文字列__ による __名前__ を使って __要素__ を __参照できる__ という __点__ は __dict に似ています__ が、__それ以外__ の __性質__ は __dict__  とは __大きく異なり__ ます。

:::note warn
__namedtuple__ はその名前の通り、__要素__ に __名前を付けられる tuple__ であり、__性質__ は __tuple に準じます__。__dict__ とは __大きく異なる__ データ型である点に __注意__ して下さい。
:::

## namedtuple のインポート

__namedtuple__ は、__collections__ モジュール内で、__クラス__ として __定義__ されているので、__利用する際__ には、__defaultdict__ と __同様の方法__ で、下記のプログラムのように、__collections__ モジュールから __インポート__ する __必要__ があります。

```python
from collections import namedtuple
```

## namedtuple のデータの作成手順

__tuple__ は、例えば __`(1, 2, 3)`__ のように __tuple__ の __リテラルを記述__ することで、__任意の数__ の __要素__ を持つ __tuple__ の __データ__ を __直接作成__ することが __できます__ が、__namedtuple__ の場合は __下記の手順__ のように、__データを作成__ する __前に__、__それぞれの要素__ に __対応__ する __フィールド名__ を __設定__ した、__tuple__ の __サブクラスを定義__ する __必要__ があります。

1. __namedtuple__ の __フィールド名__ を __設定__ した、__tuple__ の __サブクラス__ を __定義__ する
2. __サブクラス__ から、__設定__ した __フィールド名__ を __持つ__ namedtuple の __データ__ を __作成__ する

### サブクラスとは何か

__サブクラス__ とは、__特定のクラス__ を __元に__ 新しく __定義されたクラス__ の事で、__元__ となった __クラスの性質__ を __すべて受け継いだ__[^1]上で、さらなる __機能を付け加えたクラス__ の事を表します。先ほど説明したように、__namedtuple__ が、__tuple の性質__ を __すべて持ち__、__その上__ でそれぞれの __要素に名前を付ける__ ことができる __機能を持つ__ のは、__namedtuple__ が __tuple のサブクラス__ だからです。サブクラスの詳細については、今後の記事で紹介します。

### サブクラスを定義する理由

__namedtuple__ を __利用__ する際には、__多くの場合__ で __同じ意味__ を持つ __namedtuple__ の __データ__ を __複数作成__ します。例えば、__`count_marks`__ を __namedtuple__ を __返す__ ように __修正__ した場合、「__直線の手番__ のマーク、__現在の手番__ のマーク、__空のマス__」の数を __要素__ として持つ __namedtuple__ が __`count_marks`__ が __呼び出されるたび__ に __作成__ されます。

このような場合は、__一般的__ に、__共通__ の __フィールド名__ を持つ __namedtuple__ の __データ__ を __作成__ する __必要__ があります。その理由は、__同じ意味__ を持つ __namedtuple__ の __データ__ に __異なるフィールド名__ が __設定__ されていた場合は、__フィールド名__ を使って __特定の要素__ を __参照__ するプログラムを __記述__ することが __できない__（できても非常に __困難になる__）からです。

:::note
分かりづらいと思った方は、__同じもの__ に、__異なる名前__ が __ついている__ 場合のことを __思い浮かべて__ みて下さい。例えば、__1__ という数字は、__日本語__ では __いち__、__英語__ では __one__、__ドイツ語__ では __eins__ と __表記__ します。__同じ文章__ の中で、__1__ を __さまざまな言語__ で __表記__ すると __わかりづらくなる__ ので、特に理由がない限り、__ものの名前__ を __統一する__ のが __一般的__ です。これは __プログラム__ の場合でも __同様__ で、一般的に __同じ意味__ を持つ __namedtuple__ の __フィールド名__ は、__同じプログラムの中__ では __統一__ します。
:::

[以前の記事](https://qiita.com/ysgeso/items/a189461ae32a6531ff7f#%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%87%E3%83%BC%E3%82%BF%E5%9E%8B%E3%81%AE%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%A8%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9)で説明したように、__クラス__ を __自分で定義__ する事で、__任意のデータ構造__ を __管理__ し、そのデータを __処理__ する __メソッド__ を持つ __カスタムデータ型__ を定義することができます。また、定義した __クラス__ から __インスタンスを作成__ することで、__同じデータ構造__ と __機能__ をもつ __データ__ を __いくらでも作る__ ことが __できるようになります__。実際に __`Marubatsu`__ クラスを定義する事で、__〇×ゲーム__ の __機能__ を持つ __データ__ を __いくつも作成__ してきました。

__フィールド名__ を __設定__ した __tuple__ の __サブクラスを定義__ する事で、__設定__ した __フィールド名__ を持つ __namedtuple__ の __ひな形__ となる __カスタムデータ型__ を __定義__ することができます。また、その __カスタムデータ型__ から __インスタンス__ を __作成__ することで、__同じフィールド名__ を持つ __namedtuple__ の __データ__ をいくつでも、__必要なだけ作成__ することが __できるようになります__。

これが、__namedtuple__ の __データ__ を __作成__ する際に、__サブクラス__ を __定義__ する __理由__ です。

他の利点として、__サブクラスを定義__ する際に、サブクラスに __名前を付ける__ ことができるというものがあります。__異なる意味__ を持つ __namedtuple__ の __ひな形__ となる __サブクラス__ に、__適切な名前__ を付けることで、それらを __区別__ して __管理しやすくなる__ という __利点__ が生じます。

:::note warn
以後は、__わかりやすさ__ を重視して、「__tuple__ の __サブクラスの定義__」を、「__namedtuple__ の __カスタムデータ型の定義__」と __表記__ することにします。
:::

## namedtuple のカスタムデータ型の定義

__namedtuple__ の __カスタムデータ型の定義__ は、__下記__ のように __記述__ します。

```python
変数名 = namedtuple(typename, field_names)
```

__typename__ には、__定義__ する __namedtuple__ の __カスタムデータ型__（typename）の __名前__ を __文字列__ で __記述__ します。この __名前__ には、__カスタムデータ型__ が扱う __データを表す名前__ を付けるのが __一般的__ です。なお、__アラビア数字で始まる__ 名前や、__`_` 以外の記号__ が入った名前など、Python の __変数名__ や __関数名__ として __利用できない名前__ を __付ける__ ことは __できません__。

__field_names__ には、__namedtuple__ の __カスタムデータ型__ の __フィールド名__（field names）を表す __文字列__ を __先頭の要素__ から __順番__ に持つ、__list__ や __tuple__ などの __[シーケンス型](https://qiita.com/ysgeso/items/6ca248133ff4d59a81d4#%E3%82%B7%E3%83%BC%E3%82%B1%E3%83%B3%E3%82%B9%E5%9E%8B%E3%81%A8%E3%81%AF%E4%BD%95%E3%81%8B)__ で記述します。__namedtuple__ では、__すべての要素__ に __名前を付ける必要__ があるので、__filed_names__ の __要素の数__ が、__カスタムデータ型__ の __要素の数__ に __一致__ します。なお、__フィールド名__ として、それぞれの __要素の名前__ に __利用できるデータ__ には、以下のような __制限__ があります。

* __typename__ の場合と __同様__ に、__変数名__ として __利用できない名前__ をつけることは __できない__
* __`_`__（半角のアンダースコア）で __始まる名前__ は __利用できない__
* __異なる要素__ に __同じ名前__ を __付ける__ ことは __できない__

__`namedtuple`__ の __返り値__ は、作成した __カスタムデータ型__ を表す __tuple__ の __サブクラス__ で、__変数に代入__ して __利用__ します。__サブクラス__ を __代入__ する __変数の名前__ は、__一般的__ に __typename__ で __設定__ した名前と __同じ名前__ にします。

:::note
初心者が __勘違いしやすい__ 点ですが、__`namedtuple(typename, field_names)`__ によって __作成__ されるのは、__namedtuple__ の __カスタムデータ型__ であり、それ自体が __namedtuple__ の性質を持つデータ __ではない__ 点に __注意__ して下さい。
:::

### マークのパターンを表す namedtuple のカスタムデータ型の定義

上記の説明では意味が __わかりづらい__ と思いますので、__具体例__ を示します。下記の具体例を見て、__概要を理解__ してから、__もう一度__ 上記の __説明を読み直す__ ことを __お勧めします__。

__マークのパターン__ を表す __namedtuple__ の __カスタムデータ型__ は、__下記の手順__ で定義します。

1. __typename__ を __決める__。__マークのパターン__ を表すので、__`Markpat`__ という __名前にした__。なお、__クラスの名前__ は __頭文字__ を __大文字__ にする __慣習__ があるので、__頭文字__ を __大文字__ にした
2. __直前の手番__ のマーク、__現在の手番__ のマーク、__空のマス__ を表す __3 つの要素__ の __名前を決める__ 必要がある。それぞれを __`"last_turn"`__、__`"turn"`__、__`"empty"`__ と __名付ける__ ことした
3. 上記を元に、下記のプログラムを記述する

```python
Markpat = namedtuple("Markpat", [ "last_turn", "turn", "empty" ])
```

__typename__ や、それぞれの __要素の名前__ は、__文字列__ で __指定__ する必要がある点に __注意__ して下さい。また、__返り値__ を __代入__ する __変数名__ は、__一般的__ に __typename__ と __同じ名前__ にしますが、そちらは __変数名__ なので、__`""`__ で __囲ってはいけない__ 点に __注意__ して下さい。

:::note
__`field_names`__ を __一つ__ の __文字列__ で __指定__ することもできます。その場合は、__名前__ と __名前__ の __間__ を __半角の空白__ か、__半角 の `,`__ で __区切ります__。下記のプログラムは、いずれも __上記__ のプログラムと __同じ処理__ を行います。

```python
Markpat = namedtuple("Markpat", "last_turn turn empty")
Markpat = namedtuple("Markpat", "last_turn, turn, empty")
```
:::

:::note
下記のプログラムのように、__typename__ や __field_names__ に、__名前__ に __利用できない文字列__ を __指定__ すると __エラーが発生__ します。

```python
# typename の名前がアラビア数字で始まっている
namedtuple("0Markpat", ["last_turn", "turn", "empty"]) 
```

実行結果

```
略
    396     raise ValueError('Type names and field names cannot be a '
    397                      f'keyword: {name!r}')

ValueError: Type names and field names must be valid identifiers: '0Markpat'
```

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

* __ValueError__
値（Value）に関するエラー
* __Type names and field names must be valid identifiers: '0Markpat'__
'0Markpat' という、データ型（type）や（and）フィールド（field）の名前（names）は、有効（valid）な識別子（identifiers）でなければならない

```python
# フィールド名が _ で始まっている
namedtuple("Markpat", ["_last_turn", "turn", "empty"]) 
```

実行結果

```
略
    404     if name in seen:
    405         raise ValueError(f'Encountered duplicate field name: {name!r}')

ValueError: Field names cannot start with an underscore: '_last_turn'
```

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

* __ValueError__
値（Value）に関するエラー
* __ValueError: Field names cannot start with an underscore: '\_last_turn'__
'\_last_turn' というフィールド名（field names）は _（underscore）で始める（start with）ことはできない（can not）
:::


## namedtuple のインスタンスの作成

定義した __namedtuple__ の __カスタムデータ型__ から、__下記__ の方法で __インスタンスを作成__ することで、__同じフィールド名__ を持つ __namedtuple__ の __データ__ を __いくつでも作成できます__。

* __実引数__ に、__`field_names`__ で __設定した順番__ で __要素__ を __記述__ する。その際に、__`field_names`__ で __設定__ した __名前__ を使って、__キーワード引数__ で __記述__ することも __できる__
* __実引数の数__ は、__`field_names`__ の __要素の数__ に __一致__ する __必要__ がある[^2]

下記は、いずれも __`Markpat`__ から「__直前の手番__、__現在の手番__、__空のマス__」に __対応する要素__ に「__2__、__0__、__1__」を __代入__ した __データ__（インスタンス）を __作成__ するプログラムです。__キーワード引数__ で __記述__ した場合は、5 行目のように、__記述の順番__ を __変える__ ことが __できます__。

__作成__ された __データ__ を __`print`__ で __表示__ すると、__実行結果__ のような __表示__ が行われます。

```python
print(Markpat(2, 0, 1))
print(Markpat(2, 0, empty=1))
print(Markpat(2, turn=0, empty=1))
print(Markpat(last_turn=2, turn=0, empty=1))
print(Markpat(empty=1, last_turn=2, turn=0))
```

実行結果

```
Markpat(last_turn=2, turn=0, empty=1)
Markpat(last_turn=2, turn=0, empty=1)
Markpat(last_turn=2, turn=0, empty=1)
Markpat(last_turn=2, turn=0, empty=1)
Markpat(last_turn=2, turn=0, empty=1)
```

:::note
細かい話になりますが、__namedtuple__ の __カスタムデータ型__ の __データ__ を __`print`__ で __表示__ した際に、__最初に表示__ される __名前__ は、__typename__ で __指定__ した __名前__ です。__カスタムデータ型__ を __代入__ した __変数名ではない__ 点に __注意__ して下さい。

下記は、__typename__ に __"Markpat2"__ を指定し、__typename__ とは __異なる `Markpat3`__ という __変数__ に __カスタムデータ型__ を __代入__ したプログラムですが、__実行結果__ からわかるように、__最初に表示__ される __名前__ は、__typename__ に __代入__ した __Markpat2__ です。

```python
Markpat3 = namedtuple("Markpat2", [ "last_turn", "turn", "empty" ])
print(Markpat3(0, 1, 2))
```

実行結果

```
Markpat2(last_turn=0, turn=1, empty=2)
```
:::

:::note
__namedtuple__ の __カスタムデータ型__ から __データを作成__ する際に、__field_names__ の __要素数__ と __異なる数__ の __実引数__ を記述した場合は、下記のプログラムのように __エラーが発生__ します。下記は __実引数の数__ が __少ない__ 場合です。

```python
print(Markpat(0, 1))
```

実行結果

```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[8], line 1
----> 1 print(Markpat(0, 1))

TypeError: Markpat.__new__() missing 1 required positional argument: 'empty'
```

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

* __TypeError__
データ型（type）に関するエラー
* __Markpat.__new__() missing 1 required positional argument: 'empty'__
Markpat からインスタンスを作成する際に呼び出される `__new__`[^3] メソッドで必要とされる（required) 仮引数 empty に対応する位置引数（positional argument）が存在しない（missing）

下記は __実引数の数__ が __多い__ 場合です。

```python
print(Markpat(0, 1, 2, 3))
```

実行結果

```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[9], line 1
----> 1 print(Markpat(0, 1, 2, 3))

TypeError: Markpat.__new__() takes 4 positional arguments but 5 were given
```

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

* __TypeError__
データ型（type）に関するエラー
* __Markpat.__new__() takes 4 positional arguments but 5 were given__
Markpat からインスタンスを作成する際に呼び出される `__new__` メソッドは 4 つの位置引数（positional argument）を持つ（takes）が、5 つの位置引数が記述されている（were given）
:::

## フィールド名のデフォルト値の設定方法

__namedtuple__ の __フィールド名__ に __デフォルト値__ を __設定__ する方法について説明します。__本記事__ では __利用しない__ ので、興味がない方は __読み飛ばし__ ても __構いません__。

__namedtuple__ の __カスタムデータ型__ を __定義__ する際に、 __実引数__ に __`defaluts=デフォルト値を表すシーケンス型`__ という __キーワード引数__[^4] を __記述__ することで、__それぞれ__ の __要素__ の __デフォルト値__ を __設定__ することが __できます__。例えば、下記のプログラムは、__`x`__、__`y`__ という __名前__ が付けられた __要素__ に、それぞれ __`1`__、__`2`__ という __デフォルト値__ が __設定__ された __namedtuple__ の __カスタムデータ型__ を __定義__ します。なお、この __カスタムデータ型__ は、__2 次元__ の __点（point）の座標__ のように __扱うことができる__ ので __typename__ に __`Point`__ という __名前__ を付けました。

```python
Point = namedtuple("Point", ["x", "y"], defaults=[1, 2])

# 実引数が記述されていないので、両方の要素が、デフォルト値である 1 と 2 に設定される
print(Point())       
# 1 つ目の要素が実引数に記述された 3 に、2 つ目の要素がデフォルト値である 2 に設定される
print(Point(3))      
# 2 つの要素が、実引数に記述された 4 と 5 に設定される
print(Point(4, 5))   
```

実行結果

```
Point(x=1, y=2)
Point(x=3, y=2)
Point(x=4, y=5)
```

__defaults__ に __記述__ された __要素の数__ が、__field_names__ に __記述__ された __要素の数より__ も __少ない__ 場合は、__field_names__ の __先頭__ から __足りない分__ の数の __要素__ には __デフォルト値__ は __設定されません__。これは、[以前の記事](https://qiita.com/ysgeso/items/c5dbe62b329f7936181a#%E3%83%87%E3%83%95%E3%82%A9%E3%83%AB%E3%83%88%E5%BC%95%E6%95%B0%E3%81%A8%E3%83%87%E3%83%95%E3%82%A9%E3%83%AB%E3%83%88%E5%BC%95%E6%95%B0%E5%80%A4)で説明したように、__デフォルト引数__ は、__通常の仮引数より__ も __後に記述__ する __必要__ があるからです。

__下記__ のプログラムは、__field_names__ に __`"x"`__、__`"y"`__、__`"z"`__ という __3 つ__ の __要素の名前__ を __設定__ していますが、__defaults__ には __2 つ__ の __デフォルト値__ しか __設定__ していません。この場合は、__先頭__ の __`"x"`__ には __デフォルト値__ は __設定されず__、その後の __`"y"`__、__`"z"`__ にそれぞれ __`1`__、__`2`__ という __デフォルト値__ が __設定__ されます。なお、この __カスタムデータ型__ は、__3 次元__（3D）の __点__ の __座標__ のように __扱うことができる__ ので __typename__ に __`Point3D`__ という __名前__ を付けました。

```python
Point3D = namedtuple("Point3D", ["x", "y", "z"], defaults=[1, 2])

print(Point3D(3))
print(Point3D(4, 5))
print(Point3D(6, 7, 8))
```

実行結果
```
Point3D(x=3, y=1, z=2)
Point3D(x=4, y=5, z=2)
Point3D(x=6, y=7, z=8)
```

なお、__デフォルト値__ が __設定されていない__ 要素に __対応__ する __実引数__ は、__必ず記述__ する __必要__ があるので、下記のプログラムを実行すると __エラーが発生__ します。

```python
print(Point3D())
```

実行結果

```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[12], line 1
----> 1 print(Point3D())

TypeError: Point3D.__new__() missing 1 required positional argument: 'x'
```

## namedtuple の利用方法

:::note warn
表記が長いので、以後は __namedtuple__ の __カスタムデータ型__ から作成した __データ__ のことを、__list__ 等と __同様__ に、単に __nametuple__ と __表記__ します。
:::

次に、__namedtuple__ の __利用方法__ について説明します。

### インデックスによる要素の参照

__namedtuple__ は __tuple__ の __性質を持つ__ ので、下記のプログラムのように、__tuple__ と __同様__ に __インデックス__ を使って __要素を参照__ できます。

```python
mp = Markpat(2, 0, 1)
print(mp[0], mp[1], mp[2])
```

実行結果

```
2 0 1
```

### 要素の展開

__namedtuple__ は __tuple__ の __性質を持つ__ ので、下記のプログラムのように、__tuple__ と __同様__ に __要素の展開__ を行うことができます。

```python
mp = Markpat(2, 0, 1)
last_turn, turn, empty = mp
print(last_turn, turn, empty)
```

実行結果

```
2 0 1
```


### 名前による要素の参照

__namedtuple__ の __要素__ を __`field_names`__ で __設定した名前__ を使って __参照__ する場合は、下記のプログラムのように、__オブジェクトの属性__ と __同様の方法__ で、__`.名前`__ のように記述します。

```python
mp = Markpat(2, 0, 1)
print(mp.last_turn, mp.turn, mp.empty)
```

実行結果

```
2 0 1
```

なお、最初は __間違いやすい__ ですが、__dict__ と __異なり__、下記のプログラムのように、__`[]`__ と __要素の名前__ を使って __要素__ を __参照__ しようとすると __エラーが発生__ します。これは、エラーメッセージからわかるように __namedtuple__ は __tuple__ の __一種__ なので、__`[]`__ によって __指定できる__ のは __インデックス__ だからです。__namedtuple__ と __dict__ を __混同しない__ ように __注意__ して下さい。

```python
mp = Markpat(2, 0, 1)
print(mp["last_turn"])
```

実行結果

```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[16], line 2
      1 mp = Markpat(2, 0, 1)
----> 2 print(mp["last_turn"])

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

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

* __TypeError__
データ型（type）に関するエラー
* __tuple indices must be integers or slices, not str__
tuple のインデックス（indices）は、文字列ではなく（not str）、整数（integers）またはスライス（slices）でなければならない
:::

### 変数の値を利用した要素の参照

__dict__ のように、__変数の値__ を使って __namedtuple__ の __要素__ を __参照__ したい場合は、__`getattr`__ という __組み込み関数__ を __利用__ する必要があります。__`getattr`__ は、任意の __オブジェクト__ 対して __利用できる関数__ で、__1 つ目__ の __実引数__ に __オブジェクト__ を、__2 つ目__ の __実引数__ に __属性の名前__ を __記述__ することで、__指定した属性__ の __値__ を __返り値__ として返す処理を行います。

下記は、__`Markpat`__ から __作られた namedtuple__ の中から、__`name`__ に __代入__ された __`"last_turn"`__ という __名前__ が付けられた __要素の値__ を __取り出して表示__ するプロブラムです。

```python
mp = Markpat(2, 0, 1)
name = "last_turn"
print(getattr(mp, name))
```

実行結果

```
2
```

__`getattr`__ の __詳細__ については、下記のリンク先を参照して下さい。

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

# namedtuple を利用した `ai8s` の修正

次は、__namedtuple__ を __利用__ するように __`ai8s`__ を __修正__ します。

## `Markpat` の定義と保存場所

まず、__マークのパターン__ を表す __namedtuple__ の __カスタムデータ型__ を __定義__ する __必要__ がありますが、その __定義__ は、先程記述した __下記__ のプログラムで __行う__ ことができます。

```python
Markpat = namedtuple("Markpat", [ "last_turn", "turn", "empty" ])
```

__`Markpat`__ は、__`count_marks`__ だけでなく、この後で __`ai8s`__ を __修正する際__ に、__`ai8s`__ の __ブロックの中__ でも __利用__ します。そのため、__`Markpat`__ を __marubatsu.py の中__ で __定義__ する場合は、__ai.py__ などの __他のモジュール__ から __インポートできる__ ようにする __必要__ があります。そのためには、__`Markpat`__ を __`Marubatsu`__ クラスの __ブロックの外__ に __定義__ する __必要__ があります。

また、__`Markpat`__ は、__`Marubatsu`__ クラスの __中で__ も __利用する__ ので、本記事では __marubatsu.py の中__ で、__`Marubatsu`__ クラスを __定義する前__ に __`Markpat`__ の __定義を記述__ する事にします。後で、本記事の最後で示す `marubatsu_new.py` のリンクで確認して下さい。

## `count_marks` の修正

次に、下記のように、__`count_marks`__ が __`Markpat`__ を __利用__ するように __修正__ します。

* __15 行目__：__`Markpat`__ の __インスタンス__ を __作成__ して __返す__ ように __修正__ する

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

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

```python
from marubatsu import Marubatsu
from collections import defaultdict

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 Markpat(count[self.last_turn], count[self.turn], count[Marubatsu.EMPTY])

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

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

```diff_python
from marubatsu import Marubatsu
from collections import defaultdict

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]
+       return Markpat(count[self.last_turn], count[self.turn], count[Marubatsu.EMPTY])

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

:::note
__15 行目__ を、下記のように __キーワード引数__ で __記述__ しても __構いません__ が、 __`count[self.last_turn]`__ の中に __キーワード__ である __`last_turn`__ が __記述されている__ ので、この場合はわざわざ __キーワード引数__ を __記述__ する __必要はない__ でしょう。

```python
return Markpat(last_turn=count[self.last_turn], turn=count[self.turn], \
               empty=count[Marubatsu.EMPTY])
```
:::

### 動作の確認

__namedtuple__ は、__tuple__ の __性質を持つ__ ので、上記のように __`count_marks`__ の __返り値__ を __tuple__ から __namedtuple__ に __変更しても__、__`count_marks`__ の __返り値__ を __tuple として処理を行う__、__`ai8s`__ のようなプログラムの __処理__ には __影響__ を __及ぼしません__。従って、__上記の修正__ を __行っても__、__`ai8s`__ を __変更することなく__ そのまま __実行__ することが __できます__。

[前回の記事](https://qiita.com/ysgeso/items/af8b5dda0ddffd5c6d13#%E5%8B%95%E4%BD%9C%E3%81%AE%E7%A2%BA%E8%AA%8D-1)と __同様__ に、__`ai2`__、__`ai7s`__ と __対戦__ し、[以前の対戦結果](https://qiita.com/ysgeso/items/58754ba98c286f414ea4#%E4%BF%AE%E6%AD%A3%E3%81%AE%E7%A2%BA%E8%AA%8D)と __比較__ することで、__`ai8s`__ が __正しく動作するか__ どうかを __確認__ します。まず、__`ai2`__ と対戦します。

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

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

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

```
ai8s VS ai2
count     win    lose    draw
o        9843      11     146
x        8926     234     840
total   18769     245     986

ratio     win    lose    draw
o       98.4%    0.1%    1.5%
x       89.3%    2.3%    8.4%
total   93.8%    1.2%    4.9%
```

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

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

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

```
ai8s VS ai7s
count     win    lose    draw
o        3313     393    6294
x         377    2923    6700
total    3690    3316   12994

ratio     win    lose    draw
o       33.1%    3.9%   62.9%
x        3.8%   29.2%   67.0%
total   18.4%   16.6%   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.4 | 0.1 | 1.5 | 89.3 | 2.3 | 8.4 | 93.8 | 1.2 | 4.9 |
| 以前の __VS `ai7s`__  | 33.4 | 3.9 | 62.6 | 3.9 | 28.8 | 67.3 | 18.7 | 16.4 | 65.0 |
| 今回の __VS `ai7s`__  | 33.1 | 3.9 | 62.9 | 3.8 | 29.2 | 67.0 | 18.4 | 16.6 | 65.0 |

## `ai8s` の修正

__namedtuple__ を __利用__ することで __`ai8s`__ を、下記のプログラムのように __修正__ できます。__`(0, 2, 1)`__ から __`Markpat(last_turn=0, turn=2, empty=1)`__ のように __修正__ されたため、プログラムの __記述が長く__ なりますが、プログラムの __意味が明確になる__ という __利点__ が得られます。

ただし、__マークのパターン__ のように、__要素__ が __3 つ程度__ であれば、__`(0, 2, 1)`__ のほうが __簡潔に記述できる__ という __利点__ があるので、__tuple__ と __namedtuple__ の __どちらを利用__ したほうが __良いか__ は、__人によって判断が異なる__ のではないかと思います。__本記事__ では __namedtuple__ を __利用__ しますが、__分かりやすい__ と __思ったほう__ を __利用__ しても __かまいません__。

* __15、18 行目__：__tuple__ を、__同じ意味__ を表す __namedtuple__ に __修正__ する

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

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

```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 Markpat(last_turn=0, turn=2, empty=1) in markpats:
            return -1
        # 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
        elif Markpat(last_turn=2, turn=0, empty=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
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 (0, 2, 1) in markpats:
+       if Markpat(last_turn=0, turn=2, empty=1) in markpats:
            return -1
        # 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す       
-       elif (2, 0, 1) in markpats:
+       elif Markpat(last_turn=2, turn=0, empty=1) in markpats:
            return 1
        # それ以外の場合は評価値として 0 を返す
        else:
            return 0

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

:::note
上記の __15 行目__ の __namedtuple__ を __`Markpat(0, 2, 1)`__ のように __記述__ することも __できますが__、わざわざ __namedtuple__ を __利用__ する __意味がなくなります__。
:::

## 動作の確認

先程と __同様__ に、__`ai2`__、__`ai7s`__ と __対戦__ し、[以前の対戦結果](https://qiita.com/ysgeso/items/58754ba98c286f414ea4#%E4%BF%AE%E6%AD%A3%E3%81%AE%E7%A2%BA%E8%AA%8D)と __比較__ することで、__`ai8s`__ が __正しく動作するか__ どうかを __確認__ します。まず、__`ai2`__ と対戦します。

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

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

```
ai8s VS ai2
count     win    lose    draw
o        9820       9     171
x        8910     245     845
total   18730     254    1016

ratio     win    lose    draw
o       98.2%    0.1%    1.7%
x       89.1%    2.5%    8.5%
total   93.7%    1.3%    5.1%
```

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

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

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

```
ai8s VS ai7s
count     win    lose    draw
o        3317     431    6252
x         386    2961    6653
total    3703    3392   12905

ratio     win    lose    draw
o       33.2%    4.3%   62.5%
x        3.9%   29.6%   66.5%
total   18.5%   17.0%   64.5%
```

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

| 関数名 | 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.2 | 0.1 | 1.7 | 89.1 | 2.5 | 8.5 | 93.7 | 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.2 | 4.3 | 62.5 | 3.9 | 29.6 | 66.5 | 18.5 | 17.0 | 64.5 |

# NamedTuple

__Python__ には、__namedtuple__ に、__要素__ の __データ型のヒント__ を表す __[型アノテーション](https://qiita.com/ysgeso/items/4d55e73d6571fe85aa49#%E9%96%A2%E6%95%B0%E3%81%AE%E5%BC%95%E6%95%B0%E3%81%A8%E8%BF%94%E3%82%8A%E5%80%A4%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E5%9E%8B%E3%81%AE%E3%83%92%E3%83%B3%E3%83%88%E5%9E%8B%E3%82%A2%E3%83%8E%E3%83%86%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E8%A1%A8%E8%A8%98)__ の __機能を追加__ した __NamedTuple__ という __クラス__ が __typing モジュール__ に __定義__ されています。__NamedTuple__ と __namedtuple__ の __違い__ は __以下__ の __3 点__ で、__それ以外__ の __機能は同じ__ です。

* __NamedTuple__ では、それぞれの __要素__ に __型アノテーション__ を __指定できる__
* __カスタムデータ型__（tuple のサブクラス）の __定義__ の __方法__ が __異なる__
* __docstring__ を __記述できる__

特に理由がなければ、__NamedTuple__ は、__namedtuple__ の __機能を拡張__ したものなので、__NamedTuple__ のほうを __利用したほうが良い__ でしょう。そこで、__NamedTuple__ の __使い方__ について __説明__ し、__`ai8s`__ を __NamedTuple__ を __利用__ するように __修正__ することにします。

__NamedTuple__ の __詳細__ については、下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/library/typing.html#typing.NamedTuple

## NamedTuple のインポート

__NamedTuple__ は __typing__ モジュールで __定義__ されているので、__利用__ する __際__ には、下記のプログラムで __インポート__ する __必要__ があります。

```python
from typing import NamedTuple
```

## NamedTuple によるカスタムデータ型の定義

__NamedTuple__ による __カスタムデータ型__ の __定義__ は、__下記__ のように __記述__ します。理由は後で説明しますが、__型アノテーション__ を __省略__ すると __エラーが発生__ する点に __注意__ して下さい。

```python
class クラスの名前(NamedTuple):
    要素の名前: 型アノテーション # この行を要素の数だけ記述する
```

* __1 行目__：通常の __クラスの定義__ と __同様の記述__ を行うが、__クラスの名前__ の __直後__ に __`(NamedTuple)`__[^5]を __記述__ する点が __異なる__。__namedtuple__ と __異なり__、__カスタムデータ型__ の __名前__ は __typename ではなく__、__クラスの名前__ で __設定__ する 
* __2 行目以降__：__先頭の要素__ から __順番__ に、__1 行ずつ__、__`要素の名前: 型アノテーション`__ の __形式__ で __要素の名前__ と、その要素の __データ型のヒント__ を __記述__ する

__namedtuple__ で __定義__ した場合と __同じフィールド名__ を持つ __`Markpat`__ を、__NamedTuple__ で __定義__ するプログラムは、下記のように記述します。それぞれの __要素__ に __代入__ する __値__ は、__整数__ なので、それぞれの __要素__ の __型アノテーション__ には __整数型__ を表す __int__ を __記述__ しています。

```python
class Markpat(NamedTuple):   
    last_turn: int
    turn: int
    empty: int
```

また、__1 行目__ の __直後__ に、__通常のクラス__ と __同様__ に __[docstring](https://qiita.com/ysgeso/items/4d55e73d6571fe85aa49#docstring)__ を __記述__ することが __できます__。__docstring__ を __記述__ する際に、__属性の名前__ は __[Attributes セクション](https://qiita.com/ysgeso/items/a189461ae32a6531ff7f#%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AB%E5%AF%BE%E3%81%99%E3%82%8B-docstring)__ に __記述__ します。

下記は、__上記__ のプログラムに __docstring__ を __記述__ したものです。

```python
class Markpat(NamedTuple):
    """マークのパターン.
    
    Attributes:
        last_turn (int):
            直前のターンのマークの数
        turn (int):
            現在のターンのマークの数
        empty (int):
            空のマスの数
    """
    
    last_turn: int
    turn: int
    empty: int
```


## NamedTuple のインスタンスの作成

:::note warn
__namedtuple__ の場合と __同様__ に、以後は __NamedTuple__ の __カスタムデータ型__ から作成した __データ__ のことを、単に __NameTuple__ と __表記__ します。
:::

__NamedTuple__ の __カスタムデータ型__ を __定義した後__ は、__namedtuple__ の場合と __同じ方法__ で __インスタンス__ を __作成__ します。下記は、上記の __`Markpat`__ から __NamedTuple__ を __作成__ するプログラムで、__先程__ の __namedtuple__ で __紹介__ した __すべての方法__ が __そのまま利用__ できます。従って、__実行結果__ は、__先程__ と __全く同じ__ になります。

```python
print(Markpat(2, 0, 1))
print(Markpat(2, 0, empty=1))
print(Markpat(2, turn=0, empty=1))
print(Markpat(last_turn=2, turn=0, empty=1))
print(Markpat(empty=1, last_turn=2, turn=0))
```

実行結果

```
Markpat(last_turn=2, turn=0, empty=1)
Markpat(last_turn=2, turn=0, empty=1)
Markpat(last_turn=2, turn=0, empty=1)
Markpat(last_turn=2, turn=0, empty=1)
Markpat(last_turn=2, turn=0, empty=1)
```

## 動作の確認

__namedtuple__ と __NamedTuple__ は、__型アノテーション以外__ は __同一の性質__ を持ちます。従って、上記のように __`Markpat`__ を __NamedTuple__ の __カスタムデータ型__ に __変更__ しても、それを __利用する側__ の __`ai8s`__ を __修正__ する __必要__ は __ありません__。

先程と __同様__ に、__`ai2`__、__`ai7s`__ と __対戦__ し、[以前の対戦結果](https://qiita.com/ysgeso/items/58754ba98c286f414ea4#%E4%BF%AE%E6%AD%A3%E3%81%AE%E7%A2%BA%E8%AA%8D)と __比較__ することで、`ai8s` が __正しく動作するか__ どうかを __確認__ します。まず、__`ai2`__ と対戦します。

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

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

```
ai8s VS ai2
count     win    lose    draw
o        9845       6     149
x        8930     236     834
total   18775     242     983

ratio     win    lose    draw
o       98.5%    0.1%    1.5%
x       89.3%    2.4%    8.3%
total   93.9%    1.2%    4.9%
```

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

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

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

```
ai8s VS ai7s
count     win    lose    draw
o        3319     415    6266
x         391    2939    6670
total    3710    3354   12936

ratio     win    lose    draw
o       33.2%    4.2%   62.7%
x        3.9%   29.4%   66.7%
total   18.6%   16.8%   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.5 | 0.1 | 1.5 | 89.3 | 2.4 | 8.3 | 93.9 | 1.2 | 4.9 |
| 以前の __VS `ai7s`__  | 33.4 | 3.9 | 62.6 | 3.9 | 28.8 | 67.3 | 18.7 | 16.4 | 65.0 |
| 今回の __VS `ai7s`__  | 33.2 | 4.2 | 62.7 | 3.9 | 29.4 | 66.7 | 18.6 | 16.8 | 64.7 |

## デフォルト値の設定方法

__NamedTuple__ でも、__namedtuple__ と __同様__ に、__要素__ の __デフォルト値__ を __設定__ することが __できます__。__NamedTuple__ の場合は、__NamedTuple__ の __カスタムデータ型__ を __定義__ する際に、__デフォルト値__ を __`要素の名前: 型アノテーション = デフォルト値`__ のように記述します。__namedtuple__ と同様の理由で、__デフォルト値__ が __設定__ された __要素名__ よりも __後__ に、__デフォルト値__ が __設定されていない要素名__ を __記述__ することは __できない__ 点に __注意__ して下さい。

下記は、__namedtuple__ で __デフォルト値__ の __設定方法__ を __説明__ した際に __定義__ した __`Point3D`__ を __NamedTuple__ で __定義__ するプログラムです。__実行結果__ は、__先ほど__ と __全く同じ__ になります。

```python
class Point3D(NamedTuple):
    last_turn: int
    turn: int = 1
    empty: int = 2

print(Point3D(3))
print(Point3D(4, 5))
print(Point3D(6, 7, 8))
```

実行結果

```
Point3D(last_turn=3, turn=1, empty=2)
Point3D(last_turn=4, turn=5, empty=2)
Point3D(last_turn=6, turn=7, empty=8)
```

## NamedTuple の利点

__NamedTuple__ の __利点__ は、__NamedTuple__ の __要素を入力__ する際に、__型ノーテーション__ で __設定__ した __データ型のヒント__ が __表示__ される点です。また、__docstring__ を __記述__ することで、__NamedTuple__ の __要素を入力__ する際に __説明が表示__ されるという __利点__ が得られます。

下図は、__namedtuple__ で __定義__ した __`Markpat`__ を __入力__ した場合の図です。すぐ上に __フィールド名__ は __表示__ されますが、__データ型__ などの __情報__ は __表示されません__。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/1d465154-adcb-3404-d9ca-18a79cfab946.png" width="300">

下図は、__NamedTuple__ で __定義__ した __`Markpat`__ を __入力__ した場合の図です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3ca2be1d-7250-950e-dfa1-bb018b4dbce0.png" width="350">

図からわかるように、それぞれの __要素の名前__ の __右__ に、__データ型のヒント__（型アノテーション）が __表示__ されます。また、__docstring__ を __記述__ することで、それぞれの __要素__ の __意味__ が __表示__ されるので、データの入力が __分かりやすくなる__ という __利点__ が得られます。

## NamedTuple の限界

__NamedTuple__ による __型アノテーション__ は、__[関数の型アノテーション](https://qiita.com/ysgeso/items/4d55e73d6571fe85aa49#%E9%96%A2%E6%95%B0%E3%81%AE%E5%BC%95%E6%95%B0%E3%81%A8%E8%BF%94%E3%82%8A%E5%80%A4%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E5%9E%8B%E3%81%AE%E3%83%92%E3%83%B3%E3%83%88%E5%9E%8B%E3%82%A2%E3%83%8E%E3%83%86%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E8%A1%A8%E8%A8%98)__ と同様に、__ヒントにすぎない__ ため、__強制力__ は __ありません__。従って、下記のプログラムのように、__型アノテーション__ と __異なるデータ型__ を __要素__ に __代入__ しても __エラーは発生しない__ 点に __注意__ して下さい。

```python
print(Markpat("a", "b", "c"))
```

実行結果

```
Markpat(last_turn='a', turn='b', empty='c')
```

## 変数の型アノテーション

[以前の記事](https://qiita.com/ysgeso/items/4d55e73d6571fe85aa49#%E9%96%A2%E6%95%B0%E3%81%AE%E5%BC%95%E6%95%B0%E3%81%A8%E8%BF%94%E3%82%8A%E5%80%A4%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E5%9E%8B%E3%81%AE%E3%83%92%E3%83%B3%E3%83%88%E5%9E%8B%E3%82%A2%E3%83%8E%E3%83%86%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E8%A1%A8%E8%A8%98)では、__関数__ の __仮引数__ と __返り値__ の __型アノテーション__ を紹介しましたが、Python では、__ローカル変数__、__グローバル変数__、__[属性](https://qiita.com/ysgeso/items/a189461ae32a6531ff7f#%E5%B1%9E%E6%80%A7)__ などに対しても、__仮引数__ と __同様__ に、__`名前: 型アノテーション`__ という方法で __型アノテーション__ を __記述__ することが __できます__。

__NamedTuple__ の __型アノテーション__ は、この後で説明する、__クラス変数__ に対する、値を代入しない __型アノテーション__ の __仕組み__ を使って記述しています。

__変数__ の __型アノテーション__ は、__値を代入する__ 場合と __しない__ 場合があります。

### 値を代入する変数の型アノテーション

__`名前: 型アノテーション = 式`__ のように記述することで、__型アノテーション__ と __代入__ を __同時に行う__ ことができます。下記は、__グローバル変数__、__ローカル変数__、__クラス属性__、__インスタンスの属性__ に対する __型アノテーション__ と __代入__ を行うプログラムです。

```python
a: int = 1 # グローバル変数の型アノテーション

def f():
    b : float = 1.2  # ローカル変数の型アノテーション
    
class C:
    d : str = "text"  # クラス属性の型アノテーション
    
    def __init__(self):
        self.e : list[int] = [1, 2, 3]  # インスタンスの属性の型アノテーション
```

__JupyterLab__ では、文字を入力すると、__下図__ のような、__入力した文字__ に __関連__ する __入力の候補__ の __メニュー__ が表示されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/5d8d6394-fd40-a448-73bb-331e3de7de23.png" width="350">

この __メニューの中__ から、__入力したい変数__ の __項目の上__ に __マウスを移動__ すると、下図のように __項目の右端__ に、 __詳細を参照__ するための __＞__ の形をした __ボタンが表示__ されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/957e5afd-4ca5-e178-b85b-0e9bfeffffde.png" width="390">

:::note
上図ではこの __ボタンの右下__ に「__詳細を参照__」という __説明__ が表示されていますが、この説明は、__マウス__ をこのボタン __の上に移動__ した __場合のみ__ で __表示__ されます。
:::

__このボタン__ を __クリック__ すると、下図のように __型アノテーション__ が __右に表示__ されるようになります。この表示は右端の __× ボタン__ を __クリック__ することで __消す__ ことができます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/544394e3-eb58-84e6-2e2b-f63aaf9d963e.png" width=600">

他にも、入力した __変数の上__ に __マウスを移動__ することでも、下図のように __型アノテーション__ の __ヒント__ が __表示__ されます。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/7f0472d0-d785-0d12-13bd-63171a29e1d4.png" width="150">

### 値を代入しない変数の型アノテーション

プログラムで、__一度__ も __値を代入していない変数だけ__ を __記述__ すると、下記のプログラムのように、その __変数が定義されていない__（not defined）という __エラーが発生__ します。

```python
x
```

実行結果

```
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[33], line 1
----> 1 x

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

一方、__型アノテーション__ を __設定__ する場合は、下記のプログラムのように、__一度__ も __値を代入していない変数だけ__ を __記述__ しても __エラーにはなりません__。

```python
x: int
```

ただし、__型アノテーション__ を __設定__ しても、その __変数__ に __値が代入__ されたことには __ならない__ ので、下記のプログラムのように、その __変数__ を __表示__ すると __エラーが発生__ します。

```python
print(x)
```

実行結果

```
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[35], line 1
----> 1 print(x)

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

:::note
先程説明したように、__NamedTuple__ では、下記のプログラムのように、__型アノテーション__ を __省略__ すると、__エラーが発生__ します。その理由は、__型アノテーション__ を __設定せず__ に __変数の名前[^6]だけ__ を __記述__ した場合は、その __変数の値__ を __参照する__ ことになりますが、__値__ が __代入されていない変数__ を __参照__ することは __できないから__ です。

そのことは、__`x`__ という __名前__（name）が __定義されていない__（is not defined）という __エラーメッセージ__ から __確認__ できます。

```python
class Point(NamedTuple):   
    x
    y
```

実行結果

```
略
Cell In[36], line 2
      1 class Point(NamedTuple):   
----> 2     x
      3     y

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


## tuple の型アノテーション

[以前の記事](https://qiita.com/ysgeso/items/4d55e73d6571fe85aa49#list-%E3%81%AE%E5%A0%B4%E5%90%88%E3%81%AE%E5%9E%8B%E3%82%A2%E3%83%8E%E3%83%86%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E8%A8%98%E8%BF%B0%E6%96%B9%E6%B3%95)で、__list__ の __型アノテーション__ の __記述方法__ について __説明__ しましたが、__tuple__ の __型アノテーション__ について __説明していなかった__ ので、この機会に __説明__ します。

__list__ は、__すべての要素__ に __同じデータ型__ を __代入__ して __利用する__ ことが __想定される__ ことが __多い__ ので、__list__ の __型アノテーション__ では、__`list[str]`__ のように、__`[]` の中__ に、その __list の要素__ の __データ型__ を __1 つ__ だけ __記述__ します。

一方、__tuple__ の場合は、__それぞれの要素__ に __異なるデータ型__ を __代入__ することがあるので、__以下__ のような __方法__ で __型アノテーション__ を __記述__ します。

* __`tuple[int, str, list]`__ のように、__それぞれの要素__ に __代入__ する __データ型__ を __先頭から順番__ に __`,`__ で __区切って記述__ する
* __すべての要素__ の __データ型__ が __同じ__ 場合は、__`tuple[int, ...]`__ のように記述する。ただし、この場合は __要素の数__ を __指定__ することは __できない__ ので、__要素の数__ が __少ない場合__ は __`tuple[int, int, int]`__ のように __記述__ したほうが __ヒントの意味__ が __明確__ になる

__tuple__ の __型アノテーション__ の __詳細__ については、下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/library/typing.html#annotating-tuples

# dataclass

Python には、__NamedTuple__ に __よく似た__ 機能を持つ __dataclass__ が用意されているので、この機会に説明します。興味がない方は読み飛ばしてもらっても構いません。

なお、この後で説明しますが、__dataclass__ と __NamedTuple__ は似ている点はありますが、__異なる性質__ を持つので __使い分ける必要がある__ 点に __注意__ して下さい。

__dataclass__ は、クラスから __インスタンスを作成__ する際に、特定の __属性__ の __値__ を __設定__ する __処理__ を __簡潔に記述__ できる __機能__ や、__インスタンス__ の __比較__ を行う __機能__ などを持ちます。

__わかりづらい__ と思いますので、__具体例__ を挙げながら __説明__ します。

## dataclass を使わない実装方法

__インスタンス__ を __作成__ する際に、__`x`__ と __`y`__ という __属性の値__ を、__実引数__ で __設定__ する __クラス__ を、この後で説明する __dataclass__ を __使わず定義__ する場合は、下記プログラムのように __`__init__` メソッド__ を使って __定義__ します。

なお、このクラスは __2 次元の座標__ を表すデータとして考えることができるので、__namedtuple__ での __例__ の場合と __同様__ に、__`Point`__ という名前で定義しました。

```Python
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
```

この __クラス__ から、下記のプログラムのように __`x`__ と __`y` 属性__ に、__実引数__ で __記述__ した __値が代入__ されえた __インスタンス__ を __作成__ することができます。

```python
p = Point(1, 2)
print(p.x, p.y)
```

実行結果

```
1 2
```

__この方法__ による __クラスの定義__ には、下記のような __欠点__ があります。

### 属性の数が多くなった場合の欠点

__属性の数__ が __多くなった場合__ は、上記の方法では、__`__init__`__ メソッドの __記述__ が下記のように __大変__ になります。

* __仮引数__ を、__属性の数__ だけ __記述__ する __必要__ がある
* __代入文__ を __属性の数__ だけ __記述__ する __必要__ がある

### `print` でインスタンスを表示した場合の欠点

下記のプログラムのように、__`print`__ で __インスタンスを表示__ した場合に、__インスタンスの属性__ の __値__ が __表示されず__、あまり __意味のない情報__ が __表示__ される。

```python
p = Point(1, 2)
print(p)
```

実行結果（at の後ろの数字は毎回異なります）

```
<__main__.Point object at 0x000001D513E31A90>
```

### 比較に関する欠点

下記のプログラムのように、インスタンスの __すべての属性__ が __同じ値を持つ__ 場合でも、__`==` 演算子__ で __インスタンス__ を __比較__ した場合に __`False` になる__。

```python
p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1 == p2)
```

実行結果

```
False
```

これは、__自分で定義__ した __クラス__ から __作成__ された __インスタンス__ を __`==` 演算子__ で __比較__ した場合は、__インスタンス__ の __オブジェクトの id__ が __比較される__ からです。[以前の記事](https://qiita.com/ysgeso/items/911fafe3f80a805165d1#%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE-id%E8%AD%98%E5%88%A5%E5%AD%90)で説明したように、__異なるオブジェクト__ には __異なる id__ が __割り当てられる__ ので __`False`__ になります。

## dataclass の使い方と利点

__dataclass__ を __利用__ することで __上記の欠点__ を __解消__ することができます。

なお、__dataclass__ には、__今回の記事__ で __紹介__ する __以外__ にも、__さまざまな機能__ や __性質__ があります。__dataclass__ の __詳細__ について興味がある方は、下記の __リンク先__ を __参照__ して下さい。

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

### dataclass のインポート

__dataclass__ は __dataclasses__ という __モジュール__ で __定義__ されているので、__利用__ する際に、下記のプログラムを実行して __インポート__ する __必要__ があります。

```python
from dataclasses import dataclass
```

### dataclass によるクラスの定義とその利点

__dataclass__ を __利用__ した __クラスの定義__ は、__NamedTuple__ と __ほぼ同様__ の __方法__ で __記述__ します。__NamedTuple__ との __違い__ は、以下の __2 点__ です。

* __クラスの定義__ の __直前の行__ に __`@dataclass`__ を __記述する__
* __クラスの名前__ の __直後__ に __`(NamedTuple)`__ を __記述しない__

__`@dataclass`__ は、[以前の記事](https://qiita.com/ysgeso/items/ad5e7ddb071653fd2ac5#%E3%82%AF%E3%83%A9%E3%82%B9%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)で、__クラスメソッド__ を __定義__ する際に __クラスの定義__ の __直前__ に __記述__ した __`@classmethod`__ と __同様__ の、__デコレータ__ と呼ばれるもので、__関数やメソッド__ に対して何らかの __機能を付け加える__（飾り付ける（decorate））という __機能__ を持ちます。

下記は、先程の __`Point`__ を __dataclass__ を使って __定義__ するプログラムです。下記では記述していませんが、__通常のクラス__ と __同様__ に __docstring__ を __記述__ することも __できます__。

```python
@dataclass
class Point:
    x: int
    y: int
```

先程と異なり、__属性の数__ が __増えた場合__ でも、__`属性の名前: 型アノテーション`__ を __列挙するだけ__ で済むので、__`__init__`__ メソッドを __記述__ するより __簡潔に記述__ できます。

:::note
__NamedTuple__ と __同様__ に、__`属性名: 型アノテーション = 式`__ を __記述__ することで、__属性のデフォルト値__ を __設定__ することが __できます__。また、__NamedTuple__ と __同様__ の理由で、__型アノテーション__ を __省略__ すると __エラーが発生__ する点に __注意__ して下さい。
:::

### `print` によるインスタンスの表示の利点

__dataclass__ を __利用__ して __定義__ された __クラス__ から __作成__ された __インスタンス__ を __`print`__ で __表示__ すると、下記のプログラムのように、__インスタンスの属性__ の値が __表示__ されます。

```python
p = Point(1, 2)
print(p)
```

実行結果

```
Point(x=1, y=2)
```

### `==` による比較の利点

__dataclass__ を __利用__ して __定義__ された __クラス__ から __作成__ された __インスタンス__ を __`==`__ 演算子で __比較__ すると、下記のプログラムのように、__同じ名前__ の __属性の値__ が __すべて等しい__ 場合に __`True`__ が、__一つでも値が異なれば `False`__ が __計算__ されます。

```python
p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(3, 4)
print(p1 == p2)
print(p1 == p3)
```

実行結果

```
True
False
```

## クラスの定義の際に列挙していない属性の扱い

__dataclass__ を __利用__ して __定義__ された __クラス__ では、__クラスの定義__ の __際__ に __列挙していない__ インスタンスの __属性__ は、__`print`__ での __表示__ や、__`==` 演算子__ の __対象__ とは __なりません__。この性質は __勘違いしやすい__ ので __dataclass__ を __利用__ する際には __注意__ して下さい。

例えば、下記のプログラムは、__インスタンス__ の __作成後__ に、__クラスの定義__ の際に __列挙していない `z`__ という __属性__ に __値を代入__ していますが、__`z` の値__ は __`print`__ では __表示されません__。

```python
p = Point(1, 2)
p.z = 3
print(p)
```

実行結果

```
Point(x=1, y=2)
```

また、__下記__ のような __インスタンス__ を __`==` 演算子__ で __比較__ した場合でも、__`z`__ は __比較の対象__ とは __ならない__ ので __`True`__ になります。 

* __同じ属性の値__ を持つ、__`p1`__ と __`p2`__ を __作成__ する
* __`p1`__ と __`p2`__ の __`z`__ という __属性__ に __別の値__ を __代入__ する

```python
p1 = Point(1, 2)
p2 = Point(1, 2)
p1.z = 3
p2.z = 5
print(p1 == p2)
```

実行結果

```
True
```

## dataclass の使い道

__dataclass__ は __便利__ ですが、__常に使えば良い__ という __わけではありません__。__dataclass__ が __持つ性質__ を __うまく活用できる__ 場合に __利用__ すると良いでしょう。

例えば、__`Marubatsu`__ クラスの場合は、__下記__ のような __理由__ から __dataclass__ を __利用__ する __メリット__ が __ない__ ので、本記事では __`Marubatsu`__ クラスでは __dataclass__ を __利用しません__。

* __`Marubatsu`__ クラスから __作成__ された __インスタンスの属性__ の中で、__インスタンスの作成時__ に __実引数__ で __値を設定__ する __必要がある__ ものが __存在しない__
* __`Marubatsu`__ クラスは、__`print`__ で __ゲーム盤の情報__ を __わかりやすく表示__ する __必要__ があるため、インスタンスの __属性の値__ を __そのまま表示__ しても __意味がない__
* __`Marubatsu`__ クラスの __インスタンス__ を __`==` 演算子__ で __比較__ する __必要がない__

逆に言えば、上記で定義した __`Point`__ のように、__dataclass__ の __利点__ を __うまく活用できる__ ような __クラス__ を __定義__ する際には、__dataclass__ を __積極的に利用__ すると __良い__ でしょう。

他にも、__プログラム__ が __分かりやすくなる__ という __利点__ から、__dict__ で __表現できるデータ__ を、__dataclass__  を __定義__ して __記述する__ ということも __良く行われている__ ようです。

本記事でも、__dataclass__ を __うまく活用できる__ 場面があれば、__利用する__ ことにします。

## dataclass の仕組みと `__init__` メソッドに関する注意点

細かい話なので、__dataclass__ を __当面使わない__ と思った人は __読み飛ばして__ も __構いません__。

__dataclass__ を __利用__ する場合は、__下記の点__ に __注意__ する必要があります。

:::note warn
__自分__ で __`__init__`__ メソッドを __定義してはいけない__。
:::

その理由について説明します。

### dataclass の仕組み

__dataclass__ を __利用__ して __定義__ された __クラス__ には、__型アノテーション__ が __設定__ された __クラス属性__ に __対応する処理__ を行う __`__init__`__ メソッドが __自動的に定義__ されます。

例えば、__dataclass__ を __利用__ して __定義__ された __`Point`__ クラスには、下記の __`__init__`__ メソッドが __自動的に定義__ されます。

```python
def __init__(self, x:int, y:int):
    self.x = x
    self.y = y
```

__dataclass__ を利用することで、__`__init__`__ メソッドを __定義__ する必要が __なくなった__ のは、実は __dataclass__ のほうで __`__init__`__ の __定義の記述__ を、__代わりに行ってくれている__ からです。

### `__init__` メソッドを自分で定義してはいけない理由

__dataclass__ のほうで __`__init__`__ の __定義__ を __記述__ するということは、__自分__ で __`__init__`__ の __定義__ が __できなくなる__ ということです。例えば、下記のプログラムのように、__dataclass__ を __利用__ した __クラス__ に、__自分__ で __`__init__`__ メソッドを __定義__ すると、__dataclass__ が __自動的に定義__ した __`__init__`__ メソッドの __定義__ が、__自分で記述__ した __`__init__`__ メソッドの __定義__ で __上書き__ されてしまうことになります。

```python
@dataclass
class Point:
    x: int
    y: int

    def __init__(self):
        pass
```

__上記__ の __`Point`__ から __インスタンス__ を __作成__ すると、__上記の `__init__`__ メソッドが __実行__ されます。従って、下記のプログラムで、__これまで__ のように、__`x`__ と __`y`__ 属性に __`1`__ と __`2`__ が __代入__ された __`Point`__ の __インスタンス__ を __作成__ しようとすると、上記の __`__init__`__ メソッド が __実行__ されるため、__実引数の数__ と、__仮引数の数__ が __一致しない__ ことを表す __エラーが発生__ します。

```python
p = Point(1, 2)
```

実行結果

```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[48], line 1
----> 1 p = Point(1, 2)

TypeError: Point.__init__() takes 1 positional argument but 3 were given
```

また、下記のプログラムのように、__実引数__ を __記述しない__ ことで __`Point`__ の __インスタンス__ を __作成__ することはできますが、__dataclass__ が __定義__ した __`__init__`__ メソッドは __実行されない__ ため、__インスタンス__ の __`x`__、__`y`__ 属性に __値__ は __代入されません__。そのため、下記のプログラムを実行すると、__`x`__ という __属性__ が __定義されていない__ という __エラーが発生__ します。

```python
p = Point()
print(p.x)
```

実行結果

```
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[49], line 2
      1 p = Point()
----> 2 print(p.x)

AttributeError: 'Point' object has no attribute 'x'
```

### `__post_init__` メソッド

__dataclass__ を __利用__ して __定義__ された __クラス__ で、__インスタンス__ を __作成__ した __際__ の __処理__ を行いたい場合は、__`__post_init__`__ メソッドを __定義__ します。__`__post_init__`__ メソッドは、__dataclass__ が __定義__ した __`__init__`__ メソッドが __実行された後__[^7]で __呼び出される__ メソッドで、__仮引数__ には __`self` のみ__ を __記述__ します。下記は、__post init__ という __メッセージを表示__ する処理を行う __`__post_init__`__ メソッドを __`Point` の定義__ に __追加__ したプログラムです。

```python
@dataclass
class Point:
    x: int
    y: int

    def __post_init__(self):
        print("post init")
```

下記のプログラムを実行すると、__実行結果__ からわかるように、__インスタンス__ を __作成__ したs際に、__`post_init`__ が __呼び出され__、__メッセージが表示__ されます。

```python
print(Point(1, 2))
```

実行結果

```
post init
Point(x=1, y=2)
```

## dataclass と NamedTuple の違い

__dataclass__ と __NamedTuple__ は __似ています__ が、__下記の点__ が __異なる__ ので、__使い分ける__ 必要があります。

### ミュータブルである

__dataclass__ を __利用__ して __定義__ された __クラス__ から __作成__ された __インスタンス__ は、__ミュータブル__ な __データ__ です。従って、__ハッシュ可能__ なオブジェクト __ではない__ ので、下記のプログラムのように、__set__ の __要素に代入__ すると __エラーが発生__ します。

```python
print({Point(1, 2)})
```

実行結果

```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[52], line 1
----> 1 print({Point(1, 2)})

TypeError: unhashable type: 'Point'
```

### tuple ではない

__dataclass__ を __利用__ して __定義__ された __クラス__ から __作成__ された __インスタンス__ は、__tuple ではない__ ので、下記のプログラムのように、__展開__ によって __インスタンス__ の __属性__ を __取り出す__ ことは __できません__。この点は、__NamedTuple より__ も __不便__ と言えるでしょう。

```python
p = Point(1, 2)
x, y = p
```

実行結果

```
post init
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[53], line 2
      1 p = Point(1, 2)
----> 2 x, y = p

TypeError: cannot unpack non-iterable Point object
```

:::note
下記のプログラムのように、__クラスの定義__ の __直前__ に、__`@dataclass(frozen=True)`__ を __記述__ することで、__定義__ した __クラス__ から __作成__ された __データ__ を __イミュータブル__ で、__ハッシュ可能なオブジェクト__ にすることが __できます__。従って、下記のプログラムのように、__set の要素__ に __代入__ することが __できます__。


```python
@dataclass(frozen=True)
class Point:
    x: int
    y: int

print({Point(1, 2)})
```

実行結果
```
{Point(x=1, y=2)}
```

ただし、このようにした場合でも、__インスタンス__ は __tuple ではない__ ので、__`x, y = p`__ のような __展開__ を行うことは __できません__（先程と同様のエラーが発生します）。

__frozen__ とは __凍結する__ という意味の __英語__ で、__形が変化する水__ が __凍結する__ と __固まって変化しなくなる__ ことが、__データを変更できない__、__イミュータブル__ なデータに __似ている__ ことから、__キーワード引数__ に __この名前が付けられている__ ようです。
:::

# `enum_markpats` を利用した `ai6s` と `ai7s` の定義

`enum_markpats` を利用しない実装方法の例として残しておきたいので、本記事では採用しませんが、__`ai6s`__ と __`ai7s`__ を __`enum_markpats`__ を __利用__ して __簡潔に記述__ することが __できます__。下記は、__`ai6s`__ を __`enum_markpats`__ を使って __修正__ したプログラムです。

```python
def ai6s(mb, debug=False):
    def eval_func(mb):
        # 自分が勝利している場合は、評価値として 1 を返す
        if mb.status == mb.last_turn:
            return 1

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

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

下記のプログラムで __`ai6`__ と __対戦__ した結果、__正しく動作__ することが __確認__ できます。

```python
from ai import ai6, ai7

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

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

```
ai6s VS ai6
count     win    lose    draw
o        3140    1702    5158
x        1704    3116    5180
total    4844    4818   10338

ratio     win    lose    draw
o       31.4%   17.0%   51.6%
x       17.0%   31.2%   51.8%
total   24.2%   24.1%   51.7%
```

下記は、__`ai7s`__ を __`enum_markpats`__ を使って __修正__ したプログラムです。

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

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

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

下記のプログラムで __`ai7`__ と __対戦__ した結果、__正しく動作__ することが __確認__ できます。

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

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

```
ai7s VS ai7
count     win    lose    draw
o        2962     406    6632
x         392    2933    6675
total    3354    3339   13307

ratio     win    lose    draw
o       29.6%    4.1%   66.3%
x        3.9%   29.3%   66.8%
total   16.8%   16.7%   66.5%
```

# 今回の記事のまとめ

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

また、__NamedTuple__ を利用するように __`ai6s`__ を __定義__ する __方法__ を __説明__ しました。

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

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

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

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

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

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

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

# 次回の記事

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

[^1]:__特定のクラス__ の __性質__ を __受け継いだサブクラス__ を __定義__ することを、__クラスの継承__ と呼びます。クラスの継承については今後の記事で紹介します
[^2]:後述の、__デフォルト値__ を __設定__ した場合を __除きます__
[^3]:__`__new__`__ は __`__init__`__ と __似た性質__ を持つ __[特殊メソッド](https://qiita.com/ysgeso/items/a189461ae32a6531ff7f#%E7%89%B9%E6%AE%8A%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)__ で、__インスタンス__ が __作成された際__ に __自動的__ に __呼び出されます__。__`__new__`__ と __`__init__`__ の違いは複雑なので説明は省略します。興味がある方は、こちらの [リンク](https://docs.python.org/ja/3/reference/datamodel.html#object.__new__) 先を参照して下さい
[^4]:__デフォルト値__ を __位置引数__ で __記述__ することは __できません__
[^5]:__この部分の意味__ については、__NamedTuple__ を __利用する際__ に __理解する必要__ は __ありません__ ので今回の記事では説明を省略します。なお、__この部分__ は関数の仮引数のように見えるかもしれませんが、__仮引数ではありません__。この部分の意味については今後の記事で説明します
[^6]:下記の場合は、正確には __クラス属性__ です
[^7]:__post__ は、英語で __～の後の__ という __意味__ を持ちます