目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
これまでに作成した AI
これまでに作成した AI の アルゴリズム は以下の通りです。
ルール | アルゴリズム |
---|---|
ルール1 | 左上から順 に 空いているマス を探し、最初に見つかったマス に 着手 する |
ルール2 | ランダム なマスに 着手 する |
ルール3 |
真ん中 のマスに 優先的 に 着手 する 既に 埋まっていた場合 は ランダム なマスに 着手 する |
ルール4 |
真ん中、隅 のマスの 順 で 優先的 に 着手 する 既に 埋まっていた場合 は ランダム なマスに 着手 する |
ルール5 |
勝てる場合 に 勝つ そうでない場合は ランダム なマスに 着手 する |
ルール6 |
勝てる場合 に 勝つ そうでない場合は 相手の勝利 を 阻止 する そうでない場合は ランダム なマスに 着手 する |
ルール6改 |
勝てる場合 に 勝つ そうでない場合は 相手 が 勝利できる 着手を 行わない そうでない場合は ランダム なマスに 着手 する |
ルール7 |
真ん中 のマスに 優先的 に 着手 する そうでない場合は 勝てる場合 に 勝つ そうでない場合は 相手の勝利 を 阻止 する そうでない場合は ランダム なマスに 着手 する |
ルール7改 |
真ん中 のマスに 優先的 に 着手 する そうでない場合は 勝てる場合 に 勝つ そうでない場合は 相手 が 勝利できる 着手を 行わない そうでない場合は ランダム なマスに 着手 する |
ルール8 |
真ん中 のマスに 優先的 に 着手 する そうでない場合は 勝てる場合 に 勝つ そうでない場合は 相手 が 勝利できる 着手を 行わない そうでない場合は、次 の 自分の手番 で 勝利できる ように、「自 2 敵 0 空 1」が 1 つ以上 存在する 局面になる着手を行う そうでない場合は ランダム なマスに 着手 する |
基準となる ai2
との 対戦結果(単位は %)は以下の通りです。太字 は ai2 VS ai2
よりも 成績が良い 数値を表します。欠陥 の列は、アルゴリズム に 欠陥 があるため、ai2
との 対戦成績 が 良くても強い とは 限らない ことを表します。欠陥の詳細については、関数名のリンク先の説明を見て下さい。
関数名 | o 勝 | o 負 | o 分 | x 勝 | x 負 | x 分 | 勝 | 負 | 分 | 欠陥 |
---|---|---|---|---|---|---|---|---|---|---|
ai1 ai1s
|
78.1 | 17.5 | 4.4 | 44.7 | 51.6 | 3.8 | 61.4 | 34.5 | 4.1 | あり |
ai2 ai2s
|
58.7 | 28.8 | 12.6 | 29.1 | 58.6 | 12.3 | 43.9 | 43.7 | 12.5 | |
ai3 ai3s
|
69.3 | 19.2 | 11.5 | 38.9 | 47.6 | 13.5 | 54.1 | 33.4 | 12.5 | |
ai4 ai4s
|
83.0 | 9.5 | 7.4 | 57.2 | 33.0 | 9.7 | 70.1 | 21.3 | 8.6 | あり |
ai5 ai5s
|
81.2 | 12.3 | 6.5 | 51.8 | 39.8 | 8.4 | 66.5 | 26.0 | 7.4 | |
ai6 |
88.9 | 2.2 | 8.9 | 70.3 | 6.2 | 23.5 | 79.6 | 4.2 | 16.2 | |
ai6s |
88.6 | 1.9 | 9.5 | 69.4 | 9.1 | 21.5 | 79.0 | 5.5 | 15.5 | |
ai7 ai7s
|
95.8 | 0.2 | 4.0 | 82.3 | 2.4 | 15.3 | 89.0 | 1.3 | 9.7 | |
ai8s |
98.2 | 0.1 | 1.6 | 89.4 | 2.5 | 8.1 | 93.8 | 1.3 | 4.9 |
修正した count_marks
の問題点
前回の記事で修正した、count_marks
が 返り値 として返す tuple の 要素 の 意味 は、順番 に「直前の手番 のマーク、現在の手番 のマーク、空のマス」の数のように決めましたが、tuple は dict のよう に、それぞれの 要素の意味 を 表現 できる キー を 持たない ので、例えば (2, 0, 1)
という tuple を 見ただけ では、それぞれの 要素 が 何を意味する のかが わからない という 欠点 があります。
一方、dict は { "o": 2, "x": 0, ".": 1 }
のように、キー を 見る ことで、キーの値 が 何を表しているか を 知る ことが できます が、dict は ハッシュ可能なオブジェクトではない ため、set の 要素に代入 することは できません。
namedtuple の性質と使い方
この 問題 を 解決 する方法の一つに、tuple の それぞれの要素 に 名前がつけられた(named)、namedtuple という ハッシュ可能 な データ型 を 利用 するという 方法 があります。namedtuple は 頻繁に利用される データ型では ありません が、今回のように、知っておくと便利 な 場合がある ので、その使い方について説明します。
namedtuple では、要素 につけられた 名前 のことを フィールド名 と呼びます。
ただし、namedtuple には、以下のような 欠点 もあります。そのため、tuple の 要素の数が少ない 場合などで、tuple の それぞれの要素 が 何を表すか を 容易に覚えることができる ような場合は、無理 に namedtuple を 利用 する 必要 は ありません。
- tuple と 異なり、利用の際 に 前準備が必要 になる
- tuple より も、データの記述 が 多くなる
namedtuple の 詳細 については、下記のリンク先を参照して下さい
namedtuple の性質
namedtuple は、以下のような、tuple と dict の 性質 を 併せ持つ データ型です。
- tuple の性質 を すべて持つ
- tuple の性質 に 加えて、それぞれの要素 を dict のように 名前で参照 できる
ただし、namedtuple は 以下の表の点 で dict と 性質が異なり ます。なお、表の namedtuple に関する内容の詳細については、この後で詳しく説明します。
dict | namedtuple | |
---|---|---|
ミュータブルであるか | ミュータブル | イミュータブル |
ハッシュ可能であるか | ハッシュ可能でない | ハッシュ可能である |
名前に利用できるデータ | ハッシュ可能なオブジェクト | 文字列のみ |
名前による要素の参照方法 | [キーの名前] |
.フィールド名 (属性の参照と同じ) |
[] によって参照される値 |
キーの値 | インデックスの要素 (tuple と同じ) |
データの作成 | リテラルで直接行える | サブクラスを定義し、 インスタンスを作成する |
このように、namedtuple は、文字列 による 名前 を使って 要素 を 参照できる という 点 は dict に似ています が、それ以外 の 性質 は dict とは 大きく異なり ます。
namedtuple はその名前の通り、要素 に 名前を付けられる tuple であり、性質 は tuple に準じます。dict とは 大きく異なる データ型である点に 注意 して下さい。
namedtuple のインポート
namedtuple は、collections モジュール内で、クラス として 定義 されているので、利用する際 には、defaultdict と 同様の方法 で、下記のプログラムのように、collections モジュールから インポート する 必要 があります。
from collections import namedtuple
namedtuple のデータの作成手順
tuple は、例えば (1, 2, 3)
のように tuple の リテラルを記述 することで、任意の数 の 要素 を持つ tuple の データ を 直接作成 することが できます が、namedtuple の場合は 下記の手順 のように、データを作成 する 前に、それぞれの要素 に 対応 する フィールド名 を 設定 した、tuple の サブクラスを定義 する 必要 があります。
- namedtuple の フィールド名 を 設定 した、tuple の サブクラス を 定義 する
- サブクラス から、設定 した フィールド名 を 持つ namedtuple の データ を 作成 する
サブクラスとは何か
サブクラス とは、特定のクラス を 元に 新しく 定義されたクラス の事で、元 となった クラスの性質 を すべて受け継いだ1上で、さらなる 機能を付け加えたクラス の事を表します。先ほど説明したように、namedtuple が、tuple の性質 を すべて持ち、その上 でそれぞれの 要素に名前を付ける ことができる 機能を持つ のは、namedtuple が tuple のサブクラス だからです。サブクラスの詳細については、今後の記事で紹介します。
サブクラスを定義する理由
namedtuple を 利用 する際には、多くの場合 で 同じ意味 を持つ namedtuple の データ を 複数作成 します。例えば、count_marks
を namedtuple を 返す ように 修正 した場合、「直線の手番 のマーク、現在の手番 のマーク、空のマス」の数を 要素 として持つ namedtuple が count_marks
が 呼び出されるたび に 作成 されます。
このような場合は、一般的 に、共通 の フィールド名 を持つ namedtuple の データ を 作成 する 必要 があります。その理由は、同じ意味 を持つ namedtuple の データ に 異なるフィールド名 が 設定 されていた場合は、フィールド名 を使って 特定の要素 を 参照 するプログラムを 記述 することが できない(できても非常に 困難になる)からです。
分かりづらいと思った方は、同じもの に、異なる名前 が ついている 場合のことを 思い浮かべて みて下さい。例えば、1 という数字は、日本語 では いち、英語 では one、ドイツ語 では eins と 表記 します。同じ文章 の中で、1 を さまざまな言語 で 表記 すると わかりづらくなる ので、特に理由がない限り、ものの名前 を 統一する のが 一般的 です。これは プログラム の場合でも 同様 で、一般的に 同じ意味 を持つ namedtuple の フィールド名 は、同じプログラムの中 では 統一 します。
以前の記事で説明したように、クラス を 自分で定義 する事で、任意のデータ構造 を 管理 し、そのデータを 処理 する メソッド を持つ カスタムデータ型 を定義することができます。また、定義した クラス から インスタンスを作成 することで、同じデータ構造 と 機能 をもつ データ を いくらでも作る ことが できるようになります。実際に Marubatsu
クラスを定義する事で、〇×ゲーム の 機能 を持つ データ を いくつも作成 してきました。
フィールド名 を 設定 した tuple の サブクラスを定義 する事で、設定 した フィールド名 を持つ namedtuple の ひな形 となる カスタムデータ型 を 定義 することができます。また、その カスタムデータ型 から インスタンス を 作成 することで、同じフィールド名 を持つ namedtuple の データ をいくつでも、必要なだけ作成 することが できるようになります。
これが、namedtuple の データ を 作成 する際に、サブクラス を 定義 する 理由 です。
他の利点として、サブクラスを定義 する際に、サブクラスに 名前を付ける ことができるというものがあります。異なる意味 を持つ namedtuple の ひな形 となる サブクラス に、適切な名前 を付けることで、それらを 区別 して 管理しやすくなる という 利点 が生じます。
以後は、わかりやすさ を重視して、「tuple の サブクラスの定義」を、「namedtuple の カスタムデータ型の定義」と 表記 することにします。
namedtuple のカスタムデータ型の定義
namedtuple の カスタムデータ型の定義 は、下記 のように 記述 します。
変数名 = namedtuple(typename, field_names)
typename には、定義 する namedtuple の カスタムデータ型(typename)の 名前 を 文字列 で 記述 します。この 名前 には、カスタムデータ型 が扱う データを表す名前 を付けるのが 一般的 です。なお、アラビア数字で始まる 名前や、_
以外の記号 が入った名前など、Python の 変数名 や 関数名 として 利用できない名前 を 付ける ことは できません。
field_names には、namedtuple の カスタムデータ型 の フィールド名(field names)を表す 文字列 を 先頭の要素 から 順番 に持つ、list や tuple などの シーケンス型 で記述します。namedtuple では、すべての要素 に 名前を付ける必要 があるので、filed_names の 要素の数 が、カスタムデータ型 の 要素の数 に 一致 します。なお、フィールド名 として、それぞれの 要素の名前 に 利用できるデータ には、以下のような 制限 があります。
- typename の場合と 同様 に、変数名 として 利用できない名前 をつけることは できない
-
_
(半角のアンダースコア)で 始まる名前 は 利用できない - 異なる要素 に 同じ名前 を 付ける ことは できない
namedtuple
の 返り値 は、作成した カスタムデータ型 を表す tuple の サブクラス で、変数に代入 して 利用 します。サブクラス を 代入 する 変数の名前 は、一般的 に typename で 設定 した名前と 同じ名前 にします。
初心者が 勘違いしやすい 点ですが、namedtuple(typename, field_names)
によって 作成 されるのは、namedtuple の カスタムデータ型 であり、それ自体が namedtuple の性質を持つデータ ではない 点に 注意 して下さい。
マークのパターンを表す namedtuple のカスタムデータ型の定義
上記の説明では意味が わかりづらい と思いますので、具体例 を示します。下記の具体例を見て、概要を理解 してから、もう一度 上記の 説明を読み直す ことを お勧めします。
マークのパターン を表す namedtuple の カスタムデータ型 は、下記の手順 で定義します。
-
typename を 決める。マークのパターン を表すので、
Markpat
という 名前にした。なお、クラスの名前 は 頭文字 を 大文字 にする 慣習 があるので、頭文字 を 大文字 にした -
直前の手番 のマーク、現在の手番 のマーク、空のマス を表す 3 つの要素 の 名前を決める 必要がある。それぞれを
"last_turn"
、"turn"
、"empty"
と 名付ける ことした - 上記を元に、下記のプログラムを記述する
Markpat = namedtuple("Markpat", [ "last_turn", "turn", "empty" ])
typename や、それぞれの 要素の名前 は、文字列 で 指定 する必要がある点に 注意 して下さい。また、返り値 を 代入 する 変数名 は、一般的 に typename と 同じ名前 にしますが、そちらは 変数名 なので、""
で 囲ってはいけない 点に 注意 して下さい。
field_names
を 一つ の 文字列 で 指定 することもできます。その場合は、名前 と 名前 の 間 を 半角の空白 か、半角 の ,
で 区切ります。下記のプログラムは、いずれも 上記 のプログラムと 同じ処理 を行います。
Markpat = namedtuple("Markpat", "last_turn turn empty")
Markpat = namedtuple("Markpat", "last_turn, turn, empty")
下記のプログラムのように、typename や field_names に、名前 に 利用できない文字列 を 指定 すると エラーが発生 します。
# 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)でなければならない
# フィールド名が _ で始まっている
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
で 表示 すると、実行結果 のような 表示 が行われます。
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 の カスタムデータ型 の データ を print
で 表示 した際に、最初に表示 される 名前 は、typename で 指定 した 名前 です。カスタムデータ型 を 代入 した 変数名ではない 点に 注意 して下さい。
下記は、typename に "Markpat2" を指定し、typename とは 異なる Markpat3
という 変数 に カスタムデータ型 を 代入 したプログラムですが、実行結果 からわかるように、最初に表示 される 名前 は、typename に 代入 した Markpat2 です。
Markpat3 = namedtuple("Markpat2", [ "last_turn", "turn", "empty" ])
print(Markpat3(0, 1, 2))
実行結果
Markpat2(last_turn=0, turn=1, empty=2)
namedtuple の カスタムデータ型 から データを作成 する際に、field_names の 要素数 と 異なる数 の 実引数 を記述した場合は、下記のプログラムのように エラーが発生 します。下記は 実引数の数 が 少ない 場合です。
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)
下記は 実引数の数 が 多い 場合です。
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
という 名前 を付けました。
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 の 先頭 から 足りない分 の数の 要素 には デフォルト値 は 設定されません。これは、以前の記事で説明したように、デフォルト引数 は、通常の仮引数より も 後に記述 する 必要 があるからです。
下記 のプログラムは、field_names に "x"
、"y"
、"z"
という 3 つ の 要素の名前 を 設定 していますが、defaults には 2 つ の デフォルト値 しか 設定 していません。この場合は、先頭 の "x"
には デフォルト値 は 設定されず、その後の "y"
、"z"
にそれぞれ 1
、2
という デフォルト値 が 設定 されます。なお、この カスタムデータ型 は、3 次元(3D)の 点 の 座標 のように 扱うことができる ので typename に Point3D
という 名前 を付けました。
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)
なお、デフォルト値 が 設定されていない 要素に 対応 する 実引数 は、必ず記述 する 必要 があるので、下記のプログラムを実行すると エラーが発生 します。
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 の利用方法
表記が長いので、以後は namedtuple の カスタムデータ型 から作成した データ のことを、list 等と 同様 に、単に nametuple と 表記 します。
次に、namedtuple の 利用方法 について説明します。
インデックスによる要素の参照
namedtuple は tuple の 性質を持つ ので、下記のプログラムのように、tuple と 同様 に インデックス を使って 要素を参照 できます。
mp = Markpat(2, 0, 1)
print(mp[0], mp[1], mp[2])
実行結果
2 0 1
要素の展開
namedtuple は tuple の 性質を持つ ので、下記のプログラムのように、tuple と 同様 に 要素の展開 を行うことができます。
mp = Markpat(2, 0, 1)
last_turn, turn, empty = mp
print(last_turn, turn, empty)
実行結果
2 0 1
名前による要素の参照
namedtuple の 要素 を field_names
で 設定した名前 を使って 参照 する場合は、下記のプログラムのように、オブジェクトの属性 と 同様の方法 で、.名前
のように記述します。
mp = Markpat(2, 0, 1)
print(mp.last_turn, mp.turn, mp.empty)
実行結果
2 0 1
なお、最初は 間違いやすい ですが、dict と 異なり、下記のプログラムのように、[]
と 要素の名前 を使って 要素 を 参照 しようとすると エラーが発生 します。これは、エラーメッセージからわかるように namedtuple は tuple の 一種 なので、[]
によって 指定できる のは インデックス だからです。namedtuple と dict を 混同しない ように 注意 して下さい。
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
上記のエラーメッセージは、以下のような意味を持ちます。
-
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"
という 名前 が付けられた 要素の値 を 取り出して表示 するプロブラムです。
mp = Markpat(2, 0, 1)
name = "last_turn"
print(getattr(mp, name))
実行結果
2
getattr
の 詳細 については、下記のリンク先を参照して下さい。
namedtuple を利用した ai8s
の修正
次は、namedtuple を 利用 するように ai8s
を 修正 します。
Markpat
の定義と保存場所
まず、マークのパターン を表す namedtuple の カスタムデータ型 を 定義 する 必要 がありますが、その 定義 は、先程記述した 下記 のプログラムで 行う ことができます。
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
の インスタンス を 作成 して 返す ように 修正 する
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
行番号のないプログラム
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
修正箇所
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
15 行目 を、下記のように キーワード引数 で 記述 しても 構いません が、 count[self.last_turn]
の中に キーワード である last_turn
が 記述されている ので、この場合はわざわざ キーワード引数 を 記述 する 必要はない でしょう。
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
を 変更することなく そのまま 実行 することが できます。
前回の記事と 同様 に、ai2
、ai7s
と 対戦 し、以前の対戦結果と 比較 することで、ai8s
が 正しく動作するか どうかを 確認 します。まず、ai2
と対戦します。
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
と対戦します。
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 に 修正 する
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)
行番号のないプログラム
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)
修正箇所
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)
上記の 15 行目 の namedtuple を Markpat(0, 2, 1)
のように 記述 することも できますが、わざわざ namedtuple を 利用 する 意味がなくなります。
動作の確認
先程と 同様 に、ai2
、ai7s
と 対戦 し、以前の対戦結果と 比較 することで、ai8s
が 正しく動作するか どうかを 確認 します。まず、ai2
と対戦します。
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
と対戦します。
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 に、要素 の データ型のヒント を表す 型アノテーション の 機能を追加 した NamedTuple という クラス が typing モジュール に 定義 されています。NamedTuple と namedtuple の 違い は 以下 の 3 点 で、それ以外 の 機能は同じ です。
- NamedTuple では、それぞれの 要素 に 型アノテーション を 指定できる
- カスタムデータ型(tuple のサブクラス)の 定義 の 方法 が 異なる
- docstring を 記述できる
特に理由がなければ、NamedTuple は、namedtuple の 機能を拡張 したものなので、NamedTuple のほうを 利用したほうが良い でしょう。そこで、NamedTuple の 使い方 について 説明 し、ai8s
を NamedTuple を 利用 するように 修正 することにします。
NamedTuple の 詳細 については、下記のリンク先を参照して下さい。
NamedTuple のインポート
NamedTuple は typing モジュールで 定義 されているので、利用 する 際 には、下記のプログラムで インポート する 必要 があります。
from typing import NamedTuple
NamedTuple によるカスタムデータ型の定義
NamedTuple による カスタムデータ型 の 定義 は、下記 のように 記述 します。理由は後で説明しますが、型アノテーション を 省略 すると エラーが発生 する点に 注意 して下さい。
class クラスの名前(NamedTuple):
要素の名前: 型アノテーション # この行を要素の数だけ記述する
-
1 行目:通常の クラスの定義 と 同様の記述 を行うが、クラスの名前 の 直後 に
(NamedTuple)
5を 記述 する点が 異なる。namedtuple と 異なり、カスタムデータ型 の 名前 は typename ではなく、クラスの名前 で 設定 する -
2 行目以降:先頭の要素 から 順番 に、1 行ずつ、
要素の名前: 型アノテーション
の 形式 で 要素の名前 と、その要素の データ型のヒント を 記述 する
namedtuple で 定義 した場合と 同じフィールド名 を持つ Markpat
を、NamedTuple で 定義 するプログラムは、下記のように記述します。それぞれの 要素 に 代入 する 値 は、整数 なので、それぞれの 要素 の 型アノテーション には 整数型 を表す int を 記述 しています。
class Markpat(NamedTuple):
last_turn: int
turn: int
empty: int
また、1 行目 の 直後 に、通常のクラス と 同様 に docstring を 記述 することが できます。docstring を 記述 する際に、属性の名前 は Attributes セクション に 記述 します。
下記は、上記 のプログラムに docstring を 記述 したものです。
class Markpat(NamedTuple):
"""マークのパターン.
Attributes:
last_turn (int):
直前のターンのマークの数
turn (int):
現在のターンのマークの数
empty (int):
空のマスの数
"""
last_turn: int
turn: int
empty: int
NamedTuple のインスタンスの作成
namedtuple の場合と 同様 に、以後は NamedTuple の カスタムデータ型 から作成した データ のことを、単に NameTuple と 表記 します。
NamedTuple の カスタムデータ型 を 定義した後 は、namedtuple の場合と 同じ方法 で インスタンス を 作成 します。下記は、上記の Markpat
から NamedTuple を 作成 するプログラムで、先程 の namedtuple で 紹介 した すべての方法 が そのまま利用 できます。従って、実行結果 は、先程 と 全く同じ になります。
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
と 対戦 し、以前の対戦結果と 比較 することで、ai8s
が 正しく動作するか どうかを 確認 します。まず、ai2
と対戦します。
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
と対戦します。
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 で 定義 するプログラムです。実行結果 は、先ほど と 全く同じ になります。
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
を 入力 した場合の図です。すぐ上に フィールド名 は 表示 されますが、データ型 などの 情報 は 表示されません。
下図は、NamedTuple で 定義 した Markpat
を 入力 した場合の図です。
図からわかるように、それぞれの 要素の名前 の 右 に、データ型のヒント(型アノテーション)が 表示 されます。また、docstring を 記述 することで、それぞれの 要素 の 意味 が 表示 されるので、データの入力が 分かりやすくなる という 利点 が得られます。
NamedTuple の限界
NamedTuple による 型アノテーション は、関数の型アノテーション と同様に、ヒントにすぎない ため、強制力 は ありません。従って、下記のプログラムのように、型アノテーション と 異なるデータ型 を 要素 に 代入 しても エラーは発生しない 点に 注意 して下さい。
print(Markpat("a", "b", "c"))
実行結果
Markpat(last_turn='a', turn='b', empty='c')
変数の型アノテーション
以前の記事では、関数 の 仮引数 と 返り値 の 型アノテーション を紹介しましたが、Python では、ローカル変数、グローバル変数、属性 などに対しても、仮引数 と 同様 に、名前: 型アノテーション
という方法で 型アノテーション を 記述 することが できます。
NamedTuple の 型アノテーション は、この後で説明する、クラス変数 に対する、値を代入しない 型アノテーション の 仕組み を使って記述しています。
変数 の 型アノテーション は、値を代入する 場合と しない 場合があります。
値を代入する変数の型アノテーション
名前: 型アノテーション = 式
のように記述することで、型アノテーション と 代入 を 同時に行う ことができます。下記は、グローバル変数、ローカル変数、クラス属性、インスタンスの属性 に対する 型アノテーション と 代入 を行うプログラムです。
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 では、文字を入力すると、下図 のような、入力した文字 に 関連 する 入力の候補 の メニュー が表示されます。
この メニューの中 から、入力したい変数 の 項目の上 に マウスを移動 すると、下図のように 項目の右端 に、 詳細を参照 するための > の形をした ボタンが表示 されます。
上図ではこの ボタンの右下 に「詳細を参照」という 説明 が表示されていますが、この説明は、マウス をこのボタン の上に移動 した 場合のみ で 表示 されます。
このボタン を クリック すると、下図のように 型アノテーション が 右に表示 されるようになります。この表示は右端の × ボタン を クリック することで 消す ことができます。
他にも、入力した 変数の上 に マウスを移動 することでも、下図のように 型アノテーション の ヒント が 表示 されます。
値を代入しない変数の型アノテーション
プログラムで、一度 も 値を代入していない変数だけ を 記述 すると、下記のプログラムのように、その 変数が定義されていない(not defined)という エラーが発生 します。
x
実行結果
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[33], line 1
----> 1 x
NameError: name 'x' is not defined
一方、型アノテーション を 設定 する場合は、下記のプログラムのように、一度 も 値を代入していない変数だけ を 記述 しても エラーにはなりません。
x: int
ただし、型アノテーション を 設定 しても、その 変数 に 値が代入 されたことには ならない ので、下記のプログラムのように、その 変数 を 表示 すると エラーが発生 します。
print(x)
実行結果
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[35], line 1
----> 1 print(x)
NameError: name 'x' is not defined
先程説明したように、NamedTuple では、下記のプログラムのように、型アノテーション を 省略 すると、エラーが発生 します。その理由は、型アノテーション を 設定せず に 変数の名前6だけ を 記述 した場合は、その 変数の値 を 参照する ことになりますが、値 が 代入されていない変数 を 参照 することは できないから です。
そのことは、x
という 名前(name)が 定義されていない(is not defined)という エラーメッセージ から 確認 できます。
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 の型アノテーション
以前の記事で、list の 型アノテーション の 記述方法 について 説明 しましたが、tuple の 型アノテーション について 説明していなかった ので、この機会に 説明 します。
list は、すべての要素 に 同じデータ型 を 代入 して 利用する ことが 想定される ことが 多い ので、list の 型アノテーション では、list[str]
のように、[]
の中 に、その list の要素 の データ型 を 1 つ だけ 記述 します。
一方、tuple の場合は、それぞれの要素 に 異なるデータ型 を 代入 することがあるので、以下 のような 方法 で 型アノテーション を 記述 します。
-
tuple[int, str, list]
のように、それぞれの要素 に 代入 する データ型 を 先頭から順番 に,
で 区切って記述 する -
すべての要素 の データ型 が 同じ 場合は、
tuple[int, ...]
のように記述する。ただし、この場合は 要素の数 を 指定 することは できない ので、要素の数 が 少ない場合 はtuple[int, int, int]
のように 記述 したほうが ヒントの意味 が 明確 になる
tuple の 型アノテーション の 詳細 については、下記のリンク先を参照して下さい。
dataclass
Python には、NamedTuple に よく似た 機能を持つ dataclass が用意されているので、この機会に説明します。興味がない方は読み飛ばしてもらっても構いません。
なお、この後で説明しますが、dataclass と NamedTuple は似ている点はありますが、異なる性質 を持つので 使い分ける必要がある 点に 注意 して下さい。
dataclass は、クラスから インスタンスを作成 する際に、特定の 属性 の 値 を 設定 する 処理 を 簡潔に記述 できる 機能 や、インスタンス の 比較 を行う 機能 などを持ちます。
わかりづらい と思いますので、具体例 を挙げながら 説明 します。
dataclass を使わない実装方法
インスタンス を 作成 する際に、x
と y
という 属性の値 を、実引数 で 設定 する クラス を、この後で説明する dataclass を 使わず定義 する場合は、下記プログラムのように __init__
メソッド を使って 定義 します。
なお、このクラスは 2 次元の座標 を表すデータとして考えることができるので、namedtuple での 例 の場合と 同様 に、Point
という名前で定義しました。
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
この クラス から、下記のプログラムのように x
と y
属性 に、実引数 で 記述 した 値が代入 されえた インスタンス を 作成 することができます。
p = Point(1, 2)
print(p.x, p.y)
実行結果
1 2
この方法 による クラスの定義 には、下記のような 欠点 があります。
属性の数が多くなった場合の欠点
属性の数 が 多くなった場合 は、上記の方法では、__init__
メソッドの 記述 が下記のように 大変 になります。
- 仮引数 を、属性の数 だけ 記述 する 必要 がある
- 代入文 を 属性の数 だけ 記述 する 必要 がある
print
でインスタンスを表示した場合の欠点
下記のプログラムのように、print
で インスタンスを表示 した場合に、インスタンスの属性 の 値 が 表示されず、あまり 意味のない情報 が 表示 される。
p = Point(1, 2)
print(p)
実行結果(at の後ろの数字は毎回異なります)
<__main__.Point object at 0x000001D513E31A90>
比較に関する欠点
下記のプログラムのように、インスタンスの すべての属性 が 同じ値を持つ 場合でも、==
演算子 で インスタンス を 比較 した場合に False
になる。
p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1 == p2)
実行結果
False
これは、自分で定義 した クラス から 作成 された インスタンス を ==
演算子 で 比較 した場合は、インスタンス の オブジェクトの id が 比較される からです。以前の記事で説明したように、異なるオブジェクト には 異なる id が 割り当てられる ので False
になります。
dataclass の使い方と利点
dataclass を 利用 することで 上記の欠点 を 解消 することができます。
なお、dataclass には、今回の記事 で 紹介 する 以外 にも、さまざまな機能 や 性質 があります。dataclass の 詳細 について興味がある方は、下記の リンク先 を 参照 して下さい。
dataclass のインポート
dataclass は dataclasses という モジュール で 定義 されているので、利用 する際に、下記のプログラムを実行して インポート する 必要 があります。
from dataclasses import dataclass
dataclass によるクラスの定義とその利点
dataclass を 利用 した クラスの定義 は、NamedTuple と ほぼ同様 の 方法 で 記述 します。NamedTuple との 違い は、以下の 2 点 です。
-
クラスの定義 の 直前の行 に
@dataclass
を 記述する -
クラスの名前 の 直後 に
(NamedTuple)
を 記述しない
@dataclass
は、以前の記事で、クラスメソッド を 定義 する際に クラスの定義 の 直前 に 記述 した @classmethod
と 同様 の、デコレータ と呼ばれるもので、関数やメソッド に対して何らかの 機能を付け加える(飾り付ける(decorate))という 機能 を持ちます。
下記は、先程の Point
を dataclass を使って 定義 するプログラムです。下記では記述していませんが、通常のクラス と 同様 に docstring を 記述 することも できます。
@dataclass
class Point:
x: int
y: int
先程と異なり、属性の数 が 増えた場合 でも、属性の名前: 型アノテーション
を 列挙するだけ で済むので、__init__
メソッドを 記述 するより 簡潔に記述 できます。
NamedTuple と 同様 に、属性名: 型アノテーション = 式
を 記述 することで、属性のデフォルト値 を 設定 することが できます。また、NamedTuple と 同様 の理由で、型アノテーション を 省略 すると エラーが発生 する点に 注意 して下さい。
print
によるインスタンスの表示の利点
dataclass を 利用 して 定義 された クラス から 作成 された インスタンス を print
で 表示 すると、下記のプログラムのように、インスタンスの属性 の値が 表示 されます。
p = Point(1, 2)
print(p)
実行結果
Point(x=1, y=2)
==
による比較の利点
dataclass を 利用 して 定義 された クラス から 作成 された インスタンス を ==
演算子で 比較 すると、下記のプログラムのように、同じ名前 の 属性の値 が すべて等しい 場合に True
が、一つでも値が異なれば False
が 計算 されます。
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
では 表示されません。
p = Point(1, 2)
p.z = 3
print(p)
実行結果
Point(x=1, y=2)
また、下記 のような インスタンス を ==
演算子 で 比較 した場合でも、z
は 比較の対象 とは ならない ので True
になります。
-
同じ属性の値 を持つ、
p1
とp2
を 作成 する -
p1
とp2
のz
という 属性 に 別の値 を 代入 する
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 を 利用 する場合は、下記の点 に 注意 する必要があります。
自分 で __init__
メソッドを 定義してはいけない。
その理由について説明します。
dataclass の仕組み
dataclass を 利用 して 定義 された クラス には、型アノテーション が 設定 された クラス属性 に 対応する処理 を行う __init__
メソッドが 自動的に定義 されます。
例えば、dataclass を 利用 して 定義 された Point
クラスには、下記の __init__
メソッドが 自動的に定義 されます。
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__
メソッドの 定義 で 上書き されてしまうことになります。
@dataclass
class Point:
x: int
y: int
def __init__(self):
pass
上記 の Point
から インスタンス を 作成 すると、上記の __init__
メソッドが 実行 されます。従って、下記のプログラムで、これまで のように、x
と y
属性に 1
と 2
が 代入 された Point
の インスタンス を 作成 しようとすると、上記の __init__
メソッド が 実行 されるため、実引数の数 と、仮引数の数 が 一致しない ことを表す エラーが発生 します。
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
という 属性 が 定義されていない という エラーが発生 します。
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
の定義 に 追加 したプログラムです。
@dataclass
class Point:
x: int
y: int
def __post_init__(self):
print("post init")
下記のプログラムを実行すると、実行結果 からわかるように、インスタンス を 作成 したs際に、post_init
が 呼び出され、メッセージが表示 されます。
print(Point(1, 2))
実行結果
post init
Point(x=1, y=2)
dataclass と NamedTuple の違い
dataclass と NamedTuple は 似ています が、下記の点 が 異なる ので、使い分ける 必要があります。
ミュータブルである
dataclass を 利用 して 定義 された クラス から 作成 された インスタンス は、ミュータブル な データ です。従って、ハッシュ可能 なオブジェクト ではない ので、下記のプログラムのように、set の 要素に代入 すると エラーが発生 します。
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 より も 不便 と言えるでしょう。
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
下記のプログラムのように、クラスの定義 の 直前 に、@dataclass(frozen=True)
を 記述 することで、定義 した クラス から 作成 された データ を イミュータブル で、ハッシュ可能なオブジェクト にすることが できます。従って、下記のプログラムのように、set の要素 に 代入 することが できます。
@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
を使って 修正 したプログラムです。
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
と 対戦 した結果、正しく動作 することが 確認 できます。
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
を使って 修正 したプログラムです。
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
と 対戦 した結果、正しく動作 することが 確認 できます。
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 のファイルを見ることができます。
以下のリンクは、今回の記事で更新した marubatsu.py です。
以下のリンクは、今回の記事で更新した ai.py です。
次回の記事
-
特定のクラス の 性質 を 受け継いだサブクラス を 定義 することを、クラスの継承 と呼びます。クラスの継承については今後の記事で紹介します ↩
-
後述の、デフォルト値 を 設定 した場合を 除きます ↩
-
__new__
は__init__
と 似た性質 を持つ 特殊メソッド で、インスタンス が 作成された際 に 自動的 に 呼び出されます。__new__
と__init__
の違いは複雑なので説明は省略します。興味がある方は、こちらの リンク 先を参照して下さい ↩ -
デフォルト値 を 位置引数 で 記述 することは できません ↩
-
この部分の意味 については、NamedTuple を 利用する際 に 理解する必要 は ありません ので今回の記事では説明を省略します。なお、この部分 は関数の仮引数のように見えるかもしれませんが、仮引数ではありません。この部分の意味については今後の記事で説明します ↩
-
下記の場合は、正確には クラス属性 です ↩
-
post は、英語で ~の後の という 意味 を持ちます ↩