目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
これまでに作成した 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 |
前回の記事で修正した enum_markpats
の問題点
前回の記事の最後で、enum_markpats
の 返り値 を list から set に 変更 するために、下記のような修正を行いました。
-
4 行目:
markpats
に 代入 する値を、空の list から 空の set に 修正 する -
9、11、15、18 行目:
append
メソッドを、add
メソッドに 修正 する
1 from marubatsu import Marubatsu
2
3 def enum_markpats(self):
4 markpats = set()
5
6 # 横方向と縦方向の判定
7 for i in range(self.BOARD_SIZE):
8 count = self.count_marks(coord=[0, i], dx=1, dy=0)
9 markpats.add(count)
10 count = self.count_marks(coord=[i, 0], dx=0, dy=1)
11 markpats.add(count)
12 # 左上から右下方向の判定
14 count = self.count_marks(coord=[0, 0], dx=1, dy=1)
15 markpats.add(count)
16 # 右上から左下方向の判定
17 count = self.count_marks(coord=[2, 0], dx=-1, dy=1)
18 markpats.add(count)
19
20 return markpats
21
22 Marubatsu.enum_markpats = enum_markpats
行番号のないプログラム
from marubatsu import Marubatsu
def enum_markpats(self):
markpats = set()
# 横方向と縦方向の判定
for i in range(self.BOARD_SIZE):
count = self.count_marks(coord=[0, i], dx=1, dy=0)
markpats.add(count)
count = self.count_marks(coord=[i, 0], dx=0, dy=1)
markpats.add(count)
# 左上から右下方向の判定
count = self.count_marks(coord=[0, 0], dx=1, dy=1)
markpats.add(count)
# 右上から左下方向の判定
count = self.count_marks(coord=[2, 0], dx=-1, dy=1)
markpats.add(count)
return markpats
Marubatsu.enum_markpats = enum_markpats
修正箇所
from marubatsu import Marubatsu
def enum_markpats(self):
- markpats = []
+ markpats = set()
# 横方向と縦方向の判定
for i in range(self.BOARD_SIZE):
count = self.count_marks(coord=[0, i], dx=1, dy=0)
- markpats.append(count)
+ markpats.add(count)
count = self.count_marks(coord=[i, 0], dx=0, dy=1)
- markpats.append(count)
+ markpats.add(count)
# 左上から右下方向の判定
count = self.count_marks(coord=[0, 0], dx=1, dy=1)
- markpats.append(count)
+ markpats.add(count)
# 右上から左下方向の判定
count = self.count_marks(coord=[2, 0], dx=-1, dy=1)
- markpats.append(count)
+ markpats.add(count)
return markpats
Marubatsu.enum_markpats = enum_markpats
しかし、以前の記事で enum_markpats
の 実装 を 確認 する際に記述した下記のプログラムを実行すると、実行結果 のような エラーが発生 することがわかりました。
from pprint import pprint
mb = Marubatsu()
print(mb)
pprint(mb.enum_markpats())
mb.move(1, 1)
print(mb)
pprint(mb.enum_markpats())
mb.move(0, 0)
print(mb)
pprint(mb.enum_markpats())
mb.move(1, 0)
print(mb)
pprint(mb.enum_markpats())
実行結果
略
Cell In[1], line 9
7 for i in range(self.BOARD_SIZE):
8 count = self.count_marks(coord=[0, i], dx=1, dy=0)
----> 9 markpats.add(count)
10 count = self.count_marks(coord=[i, 0], dx=0, dy=1)
11 markpats.add(count)
TypeError: unhashable type: 'collections.defaultdict'
このエラーは、ハッシュ可能でない defaultdict を set の 要素に追加 しようとしたことが 原因 なので、count_marks
が ハッシュ可能なオブジェクト を 返す ように 修正 する 必要 があります。どのように修正すれば良いかについて少し考えてみて下さい。
dict のハッシュ可能なオブジェクトへの変換 その 1
これまでに紹介した、複数 の データ を 扱う ことができる ハッシュ可能 な データ型 には tuple があるので、まず defaultdict を tuple に変換 する 方法 を 考える ことにします。
defaultdict は、既定値 に関する処理 以外 では dict と 同じ性質 を持ちます。今回の記事で紹介する、defaultdict を tuple に変換 する 方法 では、既定値 に 関する処理 は 行わない ので、dict と defaultdict は、同じ方法 で tuple に変換 できます。
本記事では、以後 は 既定値 に関する処理を 行わない 場面で、dict と 記述 した場合は、defaultdict を含める ことにします。
count_marks
の修正
count_marks
の 返り値 である defaultdict(以後 は dict と 表記)は、〇、×、空 の 数 を表す 3 つのデータ を 持ちます。従って、その dict を、それらの 3 つの値 を 要素 として持つ tuple に変換 することが 可能 です。変換後 の tuple の 3 つの要素 が表すデータの 順番 は、どのような順番でもかまいませんが、何らかの順番 を 決める必要 があります。そこで、本記事では 〇、×、空 の 順番 で、数えた数 を 要素 として持つ tuple に変換 することにし、下記のプログラムのように、count_marks
を 修正 します。
-
11 行目:
count
からMarubatsu.CIRCLE
、Marubatsu.CROSS
、Marubatsu.EMPTY
の 順 で キーの値 を 取り出し、それらを要素 とする tuple を 返り値 として 返す ようにする
1 from collections import defaultdict
2
3 def count_marks(self, coord, dx, dy):
4 x, y = coord
5 count = defaultdict(int)
6 for _ in range(self.BOARD_SIZE):
7 count[self.board[x][y]] += 1
8 x += dx
9 y += dy
10
11 return count[Marubatsu.CIRCLE], count[Marubatsu.CROSS], count[Marubatsu.EMPTY]
12
13 Marubatsu.count_marks = count_marks
行番号のないプログラム
from collections import defaultdict
def count_marks(self, coord, dx, dy):
x, y = coord
count = defaultdict(int)
for _ in range(self.BOARD_SIZE):
count[self.board[x][y]] += 1
x += dx
y += dy
return count[Marubatsu.CIRCLE], count[Marubatsu.CROSS], count[Marubatsu.EMPTY]
Marubatsu.count_marks = count_marks
修正箇所
from collections import defaultdict
def count_marks(self, coord, dx, dy):
x, y = coord
count = defaultdict(int)
for _ in range(self.BOARD_SIZE):
count[self.board[x][y]] += 1
x += dx
y += dy
- return count
+ return count[Marubatsu.CIRCLE], count[Marubatsu.CROSS], count[Marubatsu.EMPTY]
Marubatsu.count_marks = count_marks
動作の確認
改めて先程の下記のプログラムを実行すると、エラー が 発生しなくなります。また、実行結果 から、同じ マークのパターン のデータが 重複しなくなった ことが 確認 できます。
mb = Marubatsu()
print(mb)
pprint(mb.enum_markpats())
mb.move(1, 1)
print(mb)
pprint(mb.enum_markpats())
mb.move(0, 0)
print(mb)
pprint(mb.enum_markpats())
mb.move(1, 0)
print(mb)
pprint(mb.enum_markpats())
実行結果
Turn o
...
...
...
{(0, 0, 3)}
Turn x
...
.O.
...
{(1, 0, 2), (0, 0, 3)}
Turn o
X..
.o.
...
{(0, 1, 2), (1, 0, 2), (1, 1, 1), (0, 0, 3)}
Turn x
xO.
.o.
...
{(0, 0, 3), (2, 0, 1), (0, 1, 2), (1, 0, 2), (1, 1, 1)}
count_marks
の問題点
初心者の方は気づいていない人が多いと思いますが、実は、上記の count_marks
の 修正 により、新しい問題 が 発生 しています。それが何かについて少し考えてみて下さい。
具体的には、下記のプログラムのように、ai6s
どうし で 対戦 を行うと エラーが発生 するという問題です。
from ai import ai_match, ai2, ai6s, ai7s
ai_match(ai=[ai6s, ai6s])
実行結果(実行結果はランダムなので下記とは異なる場合があります)
略
442 for i in range(mb.BOARD_SIZE):
443 count = mb.count_marks(coord=[0, i], dx=1, dy=0)
--> 444 if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
445 return -1
446 count = mb.count_marks(coord=[i, 0], dx=0, dy=1)
TypeError: tuple indices must be integers or slices, not str
上記のエラーメッセージは、以下のような意味を持ちます。
-
TypeError
データ型(type)に関するエラー -
invalid syntax
tuple のインデックス(indices1)は、文字列ではなく(not str)、整数(integers)またはスライス(slices)でなければならない
エラーメッセージ から、この エラー は、下記の 2 行目 で 発生する ことが分かります。
count = mb.count_marks(coord=[0, i], dx=1, dy=0)
---> if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
上記の 1 行目 の処理で、count
には count_marks
の 返り値 が 代入 されますが、その データ型 は先程 count_marks
を 修正 したため tuple になります。
2 行目 の count[mb.turn]
の インデックス に記述されている mb.turn
には、"o"
または "x"
という 文字列 が 代入 されています。tuple の インデックス は 整数 でなければならないので、2 行目の count[mb.turn]
を 計算 する際に、上記 の エラーが発生 します。
この エラー が 発生 するようになった 原因 は、ai6s
が count_marks
の 返り値 が dict であることを 前提 に 処理 を行っているにも 関わらず、count_marks
の 返り値 を dict から tuple に 修正 してしまった ため です。
互換性(compatibility)とは何か
このように、関数 の入力、処理、出力などの 仕様を変更 すると、それを利用 する プログラム が 動作しなくなる という 問題 が 発生 する 場合 が あります。
関数 に限らず、モジュール、ソフトウェア、ハードウェア などの もの を、別のもの に 置き換える ことが できる ことを、互換性がある(互換性を持つ)と呼ぶ。
例えば、さまざまなメーカー が 乾電池 を 発売 していますが、(単3電池など)同じ型 の 電池 であれば、メーカー や 製品の種類 が 異なって いても 置き換える ことが できます。このように、異なる製品 であっても、同じ型 の 電池 には 互換性 が あります。
互換性の考え方
互換性 を 考える 際に 重要 となるのは、対象となる もの を 利用する際 に 重要 となる 性質 です。例えば、電池 には さまざまな性質 がありますが、下記の表のように、電池を 電池として利用する 際に 重要となる性質 と、そうでない性質 があります。
-
重要な性質
- 大きさ(形状)
- 電圧(ボルト数)
- 容量(どれくらい長持ちするか)
- 値段
-
重要でない性質
- 重さ
- 表面のデザイン
互換性 を 考える際 には、重要な性質のみ を 考慮 し、重要でない 性質は 無視 します。
ただし、下記のような場合など、状況によって、重要となる性質 が 変わる 場合があるので、互換性 に関しては、どのような性質 を 対象 としているかを 考える必要 があります。
- 短い間 でしか 利用 しない場合は、容量 はあまり 重要ではない
- 購入済 の電池の中から 選択する よう場合は、値段 はあまり 重要はない
- 見栄え を 考慮 する必要がある のであれば、デザイン は 重要 になる
別の例として、過去は重要 であったが、現在 では 古すぎて重要ではない 性質を、互換性 の 対象から除外する ことが良くあります。
具体例 としては、2000 年頃 の パソコン で 良く使われていた 記憶装置に、フロッピーディスク がありますが、現在発売 されている パソコン で フロッピーディスク が 使えるものはない でしょう。フロッピーディスクの事が良くわからない人は、最近のパソコンで利用できなくなりつつある CD や DVD を思い浮かべて下さい。
従って、2000 年頃 は パソコンの互換性 を考える際に フロッピーディスクは重要 でしたが、現在 では フロッピーディスク は 互換性の対象 とは なりません。
互換性の分類
互換性 は、いくつかに 分類 されます。互換性の一部の 分類 は、互換性が 対象 とする 性質 を 前回の記事で紹介した 集合 として 考える と わかりやすい でしょう。
互換性 に関する 分類 は、この後で補足しますが、定義 とは 異なる意味 で 使われる場合 があります。そのため、互換性 に関する 用語 の 使われ方 が 変だと思った場合 は、文脈など からその 意味 を 判断する必要 があります。
具体例として、下記の性質を持つ 同じ会社 が 発売 した 時計 を例に挙げて説明します。
製品名 | 時刻の表示 | 日付の表示 | アラーム機能 | 発売年 |
---|---|---|---|---|
時計 A | 〇 | 2019 | ||
時計 B | 〇 | 〇 | 2020 | |
時計 C | 〇 | 〇 | 〇 | 2021 |
時計 D | 〇 | 〇 | 2022 | |
時計 E | 〇 | 2023 |
なお、後から発売 された 時計 E は、時計 A と 同じ性質 を持ちますが、時計 A とは デザインが異なる 製品だと思ってください。
完全互換(full compatible)
あるもの が、別のもの が持つ 機能 を すべて備えている ことを 完全互換 と呼びます。互換性が対象 とする 性質 を 集合 として考えると、包含関係 を持ちます。
なお、上記の時計の例にはありませんが、値段 のように、数字で表される性質 の場合は、同じか、より優れた 性質を表す 数字 である場合を 完全互換 とみなします。時計の 値段 の場合は、安いほう が 優れている とみなすことができるので、あるものの値段が、別のものの値段 以下 の場合に 完全互換 と みなします。
上記の 時計の例 の場合、以下 の 組み合わせ は 完全互換性 を持ちます。時計 A と 時計 E のように、両者の性質 が 同じ である場合も 完全互換 に 含みます。
- 「B、C、D、E」は A に対して 完全互換性を持つ
- C は B に対して 完全互換性を持つ
- C は D に対して 完全互換性を持つ
- 「A、B、C、D」は E に対して 完全互換性を持つ
X と Y を Python の set(集合)として 考える と、X が Y に対して完全互換性を持つ 場合は、X >= Y
が True
になります。
上位互換(upper compatible)
上位互換 は、完全互換 と 似ています が、両者の性質 が 同じ である場合は 含みません。これは、「上位」という 用語 が、より優れている という 意味 を表すからです。
上記の 時計の例 の場合、以下 の 組み合わせ は 上位互換性 を持ちます。時計 A と 時計 E のように、両者の性質 が 同じ 場合は 上位互換性 を 持ちません。
- 「B、C、D」は A に対して 上位互換性を持つ
- C は B に対して 上位互換性を持つ
- C は D に対して 上位互換性を持つ
- 「B、C、D」は E に対して 上位互換性を持つ
X と Y を Python の set(集合)として 考える と、X が Y に対して上位互換性を持つ 場合は、X > Y
が True
になります。
実際には、完全互換 を 上位互換 と 同じ意味 で使われる場合があります。
下位互換(lower compatible)
下位互換 は、上位互換 の 逆 で、あるもの が、別のもの の 一部 の 性質のみ を持つことを表します。上位互換 の場合と 同様 に、両者の性質 が 同じ である場合は 含みません。
上記の 時計の例 の場合、以下 の 組み合わせ は 下位互換性 を持ちます。
- 「A、E」は B に対して 下位互換性を持つ
- 「A、B、D、E」は C に対して 下位互換性を持つ
- 「A、E」は D に対して 下位互換性を持つ
時計 B と 時計 D は、お互いが持たない機能 を 持つ ので、下位互換 ではありません。
X と Y を Python の set(集合)として 考える と、X が Y に対して下位互換性を持つ 場合は、X < Y
が True
になります。
後方互換(backward compatible)
同じ目的 で 作られたもの の中で、後から作られたもの が、それより前 に 作られたもの に対して 何らか の 互換性がある ことを、後方互換 と呼びます。
時計の例 の場合、すべての時計 は 時間を表示 するという 機能を持つ ので、下記のように、後から作られた 時計は、それより前 に作られた時計に対して 後方互換性 を持ちます。
- 「B、C、D、E」は A に対して 後方互換性を持つ
- 「C、D、E」は B に対して 後方互換性を持つ
- 「D、E」は C に対して 後方互換性を持つ
- E は D に対して 後方互換性を持つ
例えば、E は D に 対して 上位互換性 を 持たない 事からわかるように、後方互換 と 上位互換 は 異なる意味 の用語です。他の 同様の例 として、機能を下げる ことによって 値段を下げた、廉価版 の製品が 後から発売 されることも良くあります。
しかし、後から作られた ものが、前に作られた ものの 改良版 である ことが多い ので、後方互換性 と 上位互換性 が 同じ意味 で使われる 場合がある 点に 注意 が必要です。
前方互換(forward compatible)
同じ目的 で 作られたもの の中で、前に作られたもの が、それより後 に 作られたもの に対して 互換性がある ことを、前方互換 と呼びます。
時計の例 の場合、すべての時計 は 時間を表示 するという 機能を持つ ので、下記のように、前に作られた 時計は、それより後 に作られた時計に対して 前方互換性 を持ちます。
- A は B に対して 前方互換性を持つ
- 「A、B」は C に対して 前方互換性を持つ
- 「A、B、C」は D に対して 前方互換性を持つ
- 「A、B、C、D」は E に対して 前方互換性を持つ
過去 に作られたものが、未来 に作られたものの 性質 を すべて持つ ことは ほとんどない ので、一般的 に 前方互換性 と 完全互換性 や 上位互換性 は 同時 に 満されません。
非互換(incompatible)
2 つのもの が 共通の性質 を 持たない ことを、非互換 と呼びます。先ほどの 時計の例 では 非互換性 を持つ 製品 の 組み合わせ は ありません。
個別の性質の互換性
ここまでで説明した互換性の分類は、ものがもつ 性質の全体 を 考慮 したものですが、それとは別 に、個別の性質 に対して 互換性 と 非互換性 を 考える 場合があります。
例えば、「アラーム機能」という 性質だけ に 注目 した場合、時計 C と 時計 D は 互換性 を 持ちます が、時計A と 時計 C は 非互換性 を持ちます。
バージョンと互換性
変更前 と 変更後 の 仕様 を 区別する ための 用語 を バージョン(version)と呼びます。バージョン を 新しくする ことを バージョンアップ と呼びます。また、何らかの理由で、古いバージョン を 利用する ことを バージョンダウン と呼びます。
バージョン は 一般的 に 数字 を使って 区別 することが多く、仕様 を 新しくするたび に 数字を増やす のが 一般的 です。
例えば、ソニーのプレーステーション(PlayStation。以後は PS と表記する)というゲーム機は PS1、PS2、PS3、PS4、PS5 という 順番 でバージョンが付けられています。
Microsoft 社の Office というソフトのバージョンは、Office 2023 のように、発売した年号 でバージョンが付けられています2。
数字以外 をバージョンにする場合もあります。例えば、Microsoft 社の Windows という OS のバージョンはかなり 変則的 で、Windos 95 のように 発売した年(1995年)の 下2桁 でバージョンをつけたり、Windows XP や Windows Me などのように、アルファベット でバージョンをつけたこともありました。なお、Windows 7 以降は、8、10、11 のように 数字の順番 でバージョンをつけています。
なお、数字の順番 でバージョンをつける際に、特定の数字 が 飛ばされる こともあります。例えば Windows 8 の次のバージョンは Windows 10 です。
メジャーバージョンとマイナーバージョン
バージョン を .(半角のピリオド)で 区切って、複数 の 数字や記号 を使って 付ける 場合があります。例えば、Python の バージョン は 3.11 のように 記述 します。また、複数 の . を使ってで 2.1.3 のように、3 つ以上 に 分ける こともあります。
バージョンの 最初 の 数字や記号 を メジャーバージョン、次 の 数字や記号 を マイナーバージョン と呼びます。メジャーバージョン や マイナーバージョン の 意味 は、場合によって異なります が、一般的 には 下記 のような 意味 を持ちます。
メジャーバージョン は、一般的 に 仕様 が 根本的に変わる ような場合に 変えられる ので、メジャーバージョン が 異なる場合 は、一般的 に 多くの性質 の 互換性 が 保たれない と 考えたほうが良い でしょう。なお、メジャーバージョン に 0 がつけられている場合は、一般的 に、正式公開前 の 未完成 の バージョン であることを表します。
マイナーバージョン は、機能の追加 や、大きな不具合の修正 などが行われた場合に 変えられます。多くの機能 は 互換性 が 保たれます が、一部の機能 は 保たれない こともあるので、マイナーバージョン が 変わった場合 は、更新情報 などを見ることを お勧めします。
それ以降 のバージョンの 数字や記号 は、互換性 を 保った ままの、微細な変更 が行われた際に変えられる場合が多く、あまり 気にする必要はない ことが 多い でしょう。
例えば、Python の場合は メジャーバージョン が 2 から 3 に 変わった際 に、仕様 が 大幅に変更 されました。従って、Python の バージョン 2 で作られた プログラム は、バージョン 3 では 多くの場合 でそのままでは 正しく動作しない と 考えたほうが良い でしょう。
本記事 では marubatsu.py
や ai.py
などの モジュール を 毎回の記事 で 更新 し、Github 内 の 記事の番号 と同じ 053
のような名前の フォルダ に モジュール の ファイルを保存 しています。本記事では モジュール に バージョン をつけて 区別していません が、フォルダ名 の 番号 を マイナーバージョン と みなす ことが できます。
なお、長くなるので説明は省略しますが、github には、フォルダにわけてバージョンを管理するよりも 洗練 された バージョン管理 の 機能を持ちます。興味がある方は調べてみると良いでしょう。
プログラムにおける互換性
下記は、今回の記事の最初で説明した、互換性 の 定義 を再掲したものです。
関数 に限らず、モジュール、ソフトウェア、ハードウェア などの もの を、別のもの に 置き換える ことが できる ことを、互換性がある(互換性を持つ)と呼ぶ。
上記の定義から、プログラム の場合は、関数 や モジュール の 機能 が 共通していても、それらを 異なるバージョン で 置き換えた時 に 動作しなくなる 場合は、互換性がある とは みなされません。例えば、count_marks
は、今回の記事の 修正前 と、修正後 で、いずれも マークの数を数えるという 共通の機能を持ちますが、バージョンアップ することで ai8s
が 正しく動作しなくなる ので 互換性 があるとは みなされません。
プログラム での 互換性 は、モジュール など、他人が作成 した プログラム を 利用する際 に 特に重要 となる 概念 です。実際 に Python そのものの バージョン や、モジュール の バージョン が 異なる と、同じプログラム が正しく 動作しなくなる 場合がある 理由 は、そのプログラム が利用している モジュール に 互換性がない ことが 原因 です。
互換性の確認方法
モジュール が バージョンアップ した際に、その モジュール の すべて の 関数の仕様 が 変化 することは ほとんどありません。どの関数 の 仕様が変化 したかについては、更新情報 を記した ドキュメント が 示される のが 一般的 です。例えば、下記のリンクは Python の バージョン 3.11 の 更新情報 が記述されている 公式のドキュメント です。
モジュールの ドキュメント の 関数 などの 仕様の説明 の所にも、一般的 に バージョン による 仕様の変更 に関する 情報 が記述されます。例えば、下記は組み込み型のクラスである、float
の 仕様を説明 する 公式ドキュメント への リンク ですが、その 説明の中 に、下記のような バージョン による 仕様の変更 の情報が 記述 されています。
「バージョン 3.7 で変更: x は位置専用引数になりました」
バージョンの確認の重要性
Python に限らず、プログラム や モジュール を 一般に公開 する際に、その プログラムを作成 した プログラム言語 の バージョン や、モジュール の バージョン などを 明記 する 場合が多い のは、互換性 が 原因 です。本記事でも、初回の記事 でそれらを 明記 しています。
他人が作成 したモジュールを バージョンアップ した際に、プログラム が 動作しなくなった場合 は、互換性 が 原因 である 可能性が高い ことを覚えておいたほうが良いでしょう。
互換性 は、ソフトウェア だけでなく、ハードウェア にも 存在 します。例えばプレーステーション(PS)というゲーム機は、PS1 と PS2 の間で 互換性があった ため、PS2 で PS1 のソフトを そのまま遊ぶ ことが できました が、PS3 と PS4 の間には 互換性がない ので、PS4 で PS3 のソフトをそのまま 遊ぶ ことは できません。
プログラムの後方互換性
プログラムでは、後方互換性 が 特に重要 です。例えば 関数 や モジュール の 仕様を更新 して バージョンアップ した際に、新しいバージョンが 後方互換性 かつ 完全互換性 を持つ場合は、古いバージョン を使って作られた プログラム を 変更することなく、そのまま利用 することが できます。上記のノート で紹介した、PS2 で PS1 のゲーム を そのまま遊べる のは、PS2 が PS1 に対して 後方互換性 と 完全互換性 を持つからです。一方、今回の記事で バージョンアップ した count_marks
は、後方互換性 を 持たない ため、バージョンアップ前 の count_marks
を 利用 していた ai6s
を 実行 すると エラーが発生 します。
モジュール などの バージョンアップ を行っても、プログラム が そのまま動作 することが多いのは、後方互換性 と 完全互換性 があるように モジュール が 作られている 場合が多いからです。例えば、Windows という OS は、ほとんどの機能 に対して 後方互換性 があるように 作られている ので、古い バージョン Windowsで 作られた ソフトの 多く は、新しいバージョン の Windows で そのまま実行 することが できます。
一般的 に、バージョンアップ時 に 互換性 と 表記 した場合は、後方互換性 と 完全互換性 の 両方の性質 を 持つ場合 の事を表します。本記事 でも以後は、バージョンアップ時 の 互換性 という 用語 を、そのような 意味 で 使用 することにします。
プログラムの前方互換性
過去のバージョン で、未来のバージョン の 機能 を 利用 できるという、前方互換性 の性質を 実現 することは、未来の予測 が 難しい ため、一般的 には 困難 です。また、プログラム の場合は、最近ではインターネットから 簡単にバージョンアップ が できるようになった ので、前方互換性 の 性質 が 求められる 場面はあまり 多くありません。そのため、バージョンアップ時 に 前方互換性 を 考慮しない ことが良くあります。本記事 でも、関数 や モジュール の バージョンアップ時 に 前方互換性 は 考慮しません。
前方互換性 は、例えば ソフトウェア が 有料 な場合など、バージョンアップ が 簡単でない場合 などで 必要とされます。前方互換性 は、将来 の バージョンアップ を 想定する ように 仕様を設計 するなどの方法で 実現 します。前方互換性 があるソフトウェアの 代表例 として、Microsoft 社の Office の製品が挙げられます。実際に Word などの Office の製品は、新しいバージョン で 作られた文章 を、古いバージョン で 開いて編集 することが 可能 です。他にも、先程例とした挙げた Windows にも 前方互換性 があり、例えば Windows 11 で作成したソフトを、多くの場合 で Windows 10 で実行 することができます。
なお、前方互換性 は、一般的 に 新しいバージョン の 一部の機能 しか 利用 できないという 限界 があります。当たり前ですが、過去のバージョン の 仕様を作成 した際に、未来のバージョン で 追加される機能 を 完全に想定 するのは ほぼ不可能 だからです。実際に Office 製品では、古いバージョン で 新しいのバージョン で作られた 文書 を 開いて編集 することは できますが、新しいバージョン で 追加 された 新しい機能 を 利用 することは できません。
具体例として、Excel という表計算ソフトは 前方互換性 を 持ちます が、IFS という関数は Excel 2016 で 新しく追加 された機能なので、それ以前のバージョン で この関数を利用 した ファイル を 開いた際 に、この関数 は 正しく動作しません。
互換性の問題を解決する方法
以後は、互換性 を、後方互換性 と 完全互換性 を持つという 意味 で 使用 します。
他人が作成 した モジュール を 利用する側 の 立場 では、互換性 が あれば、モジュールの バージョン が 変わっても、自分が記述 した プログラム を 変更 する 必要がない ので、互換性 が あったほうが便利 です。
一方、自分で作成 した モジュール を一般に 公開する側 の 立場 では、仕様を変更 する際に、その 前後のバージョン で 互換性を持つ のは、それほど 簡単 な話ではないので、互換性 を持つことが、必ずしも 望ましい とは かぎりません。
仕様を変更 した際に、互換性 の問題を 解決する ための、さまざまな方法 があります。本記事では、そのうちの、良く使われる いくつかの 方法 について 紹介 します。
仕様を変更したモジュールを利用しているプログラムを修正する
この方法は 仕様を変更 した モジュール が、互換性 を全く 持たない 方法です。
プログラムの中で、仕様を変更 した モジュール を 利用 する 記述の数 が 少ない 場合は、プログラム のほうを、新しい仕様 に 合わせて修正 するという 方法 があります。例えば、プログラムの中で モジュール を 利用 する 記述の数 が 1 つ の場合などです。
本記事 でも、これまでに 多くの関数 の 仕様 を 何度も変更 してきましたが、ほとんどの場合 で その関数 を 利用 する 記述 が 少なかった ので、この方法で修正 を行ってきました。
しかし、プログラムの中で 仕様を変更 した モジュール を 利用 する 記述の数 が 多くなる と、それらを すべて修正 するのが 大変 になります。また、その モジュール を 一般に公開 していた場合は、その モジュール を 誰がどれだけ利用 しているかが わからない ので、そのような場合は この方法 による 影響 を 予測 することは 困難 です。
なお、この方法には、上記のような 欠点 は ありますが、互換性より も 仕様の修正 のほうが 重要 な場合や、互換性 を持たせることが 困難な場合 などで、実際 に 行われます。
具体的には、バグを修正 するために 互換性 をどうしても 保てない場合 があります。特に セキュリティ に 関するバグ など、互換性 を 持つより も バグの修正 のほうが 重要な場合 は、互換性 を あきらめる ことになります。
他の例としては、仕様が古くなった 場合などで、モジュール や ソフトウェア そのものを、一から 設計し直したほう が 良い場合 などがあり、そのような更新を行う場合は、一般的 に メジャーバージョン を 更新 します。
あまりよい例えではないかもしれませんが、何十年も前の 古いクーラー を、修理しながら だましだまし 使っていく のを やめて、新しいクーラー を 買う という場合に似ています。新しいクーラーなので、今まで とは 異なる使い方 を 覚える必要 がありますが、性能 や 電気代 などの面で 得になる でしょう。
別の関数として定義する
モジュール内 の 関数の仕様 を 変更 する際に、その 関数そのもの を 修正 するの ではなく、元の関数 をそのまま 残したまま、別の名前 の 関数 として 定義する という方法があります。この方法は、元の関数 が そのまま残る ので、互換性 を 持つ 方法です。
一方、この方法には、下記 のような 問題 があります。
- 修正前 と 修正後 の プログラム が ほとんど同じ場合 は、似たような処理 を行う 関数 が 2 つ定義 されてしまう
- それぞれ の 関数 に 別の名前 を 付ける必要 があるため、修正 を 何度 も 重ねていく と、同じような名前 の 関数 が 複数定義 されてしまうため、区別が大変 になる
count_marks
に この方法を適用 する場合、下記 の 2 つ の メソッドを定義 する事になります。2 つ目 の count_marks_by_tuple
が tuple で マークのパターン を 返す メソッドですが、count_marks
と 異なる のは、最後 の return 文だけ です。
def count_marks(self, coord, dx, dy):
x, y = coord
count = defaultdict(int)
for _ in range(self.BOARD_SIZE):
count[self.board[x][y]] += 1
x += dx
y += dy
return count
def count_marks_by_tuple(self, coord, dx, dy):
x, y = coord
count = defaultdict(int)
for _ in range(self.BOARD_SIZE):
count[self.board[x][y]] += 1
x += dx
y += dy
return count[Marubatsu.CIRCLE], count[Marubatsu.CROSS], count[Marubatsu.EMPTY]
count_marks_by_tuple
の場合は、下記のプログラムのように 工夫する ことで、同じような関数 を 2 つ定義 する 問題を解決 できますが、このような工夫を 常に行える とは 限りません。また、この方法では 名前の問題 を 解決できません。
-
2 行目:
count_marks
を 呼び出して、dict の マークのパターン を 計算 する - 3 行目:dict を tuple に 変換 した値を 返す
def count_marks_by_tuple(self, coord, dx, dy):
count = self.count_marks(self, coord, dx, dy):
return count[Marubatsu.CIRCLE], count[Marubatsu.CROSS], count[Marubatsu.EMPTY]
関数のデフォルト引数を利用して互換性を持つ
関数の名前 を 変更せず に、仮引数 を使って 互換性を持つ という方法があります。この 方法 では、仮引数 に 代入された値 によって、その関数が 修正前の処理 を行うか、修正後の処理 を行うかを 変更できる ようにします。また、その際に、その 仮引数 を デフォルト引数 とし、デフォルト値 に 修正前の処理 を行う 値を設定 することで、その 関数を利用 していた プログラム を 修正する必要 が なくなります。本記事 では この方法 で count_marks
の 互換性を持つ ようにすることにします。
なお、この方法 は 実際 に 良く使われる 方法ですが、修正を重ねる たびに、関数の中 で 互換性を持つ ための 記述 が 増えていく ことになるので、下記 のような 問題 が 発生しやすく なります。そのため、ある程度以上 古くなった 過去の 仕様 に関する 互換性 を 切り捨てて プログラムを 整理する ということも 良く行われます。
- 関数の 仮引数 が 増えた 結果、使い方 が 分かりづらくなる
- 関数で 行われる処理 が 複雑になる ため 分かりづらくなる
- 上記 の 2 つ が 原因 で、バグ が 発生しやすく なる
モジュール の ドキュメント などで、非推奨(deprecated)という 用語 をよく見かけますが、これは その機能 を 互換性のため に しばらく残しておく が、将来のバージョン で 利用できなくなる予定 であるということを表します。そのような機能は、いつ使えなくなる か わからない ので、なるべく 使用しないほうが良い でしょう。
例えば、Python の 公式ドキュメント の組み込み関数 int
の説明には、下記のような、非推奨 に関する 記述 があります。
「バージョン 3.11 で変更: __trunc__() への処理の委譲は非推奨になりました」
互換性を持つ count_marks
の修正
互換性を持つ ように、count_marks
を 修正 するためには、互換性のため の 仮引数 の 名前 を 考える 必要があります。そこで、その 仮引数 を 以下 のように 定義 する事にします。
名前:返り値 の データ型(datatype)を 表す ので、datatype
という名前にする
意味:"dict"
という 文字列 の場合は dict を返し、それ以外 の場合は tuple を返す
規定値:それまでに dict が返される と 想定 してこの関数を利用していた プログラム を 修正しなくても済む ように、"dict"
を規定値 とする
上記以外にも、仮引数 を 下記 のように 定義 する方法も 考えられます。もっと良い 仮引数の定義の方法を 思いついた人 は その方法 を 採用 しても かまいません。
-
実際 に 返される 値の データ型 が defaultdict であることを考慮して、
"dict"
または"defaultdict"
が 代入 された 場合 に dict を返す ようにする - 文字列 ではなく、
dict
という 組み込み型のクラス そのものが 代入 された 場合 に、dict を返す ようにする -
仮引数 の 名前 を例えば、
return_by_dict
のようにし、True
が 代入 された 場合 に dict を、False
が 代入 された 場合 に tuple を返す ようにする
下記のプログラムは、仮引数 に datatype
を 追加 した count_marks
の 定義 です。
-
1 行目:デフォルト値 を
"dict"
とする デフォルト引数datatype
を 追加 する -
9、10 行目:
datatype
が"dict"
の場合に、dict が代入 されたcount
をそのまま 返す -
11、12 行目:そうでない 場合に、
count
を tuple に変換 した値を 返す
1 def count_marks(self, coord, dx, dy, datatype="dict"):
2 x, y = coord
3 count = defaultdict(int)
4 for _ in range(self.BOARD_SIZE):
5 count[self.board[x][y]] += 1
6 x += dx
7 y += dy
8
9 if datatype == "dict":
10 return count
11 else:
12 return count[Marubatsu.CIRCLE], count[Marubatsu.CROSS], count[Marubatsu.EMPTY]
13
14 Marubatsu.count_marks = count_marks
行番号のないプログラム
def count_marks(self, coord, dx, dy, datatype="dict"):
x, y = coord
count = defaultdict(int)
for _ in range(self.BOARD_SIZE):
count[self.board[x][y]] += 1
x += dx
y += dy
if datatype == "dict":
return count
else:
return count[Marubatsu.CIRCLE], count[Marubatsu.CROSS], count[Marubatsu.EMPTY]
Marubatsu.count_marks = count_marks
修正箇所
def count_marks(self, coord, dx, dy, datatype="dict"):
x, y = coord
count = defaultdict(int)
for _ in range(self.BOARD_SIZE):
count[self.board[x][y]] += 1
x += dx
y += dy
- return count[Marubatsu.CIRCLE], count[Marubatsu.CROSS], count[Marubatsu.EMPTY]
+ if datatype == "dict":
+ return count
+ else:
+ return count[Marubatsu.CIRCLE], count[Marubatsu.CROSS], count[Marubatsu.EMPTY]
Marubatsu.count_marks = count_marks
上記 の count_marks
は、datatype
に "dict"
以外 のデータが 代入 されている場合に tuple が 返されます。そのため、例えば datatype
に "int"
という 文字列 が 代入 されている場合でも、整数型ではなく、tuple が 返されます。その点が 気になる 方は、最後 の if 文 を 下記 のプログラムのように 修正 して下さい。
ただし、下記のように 厳密な処理 を 記述しなくても、互換性を持つ という 目的 を 達成できる ので、元のプログラム の ままでも 問題はない でしょう。本記事 でも、下記のプログラムのようには 修正しない ことにします。
-
3、4 行目:
datatype
が"tuple"
の 場合 に tuple を返す ようにする -
5 ~ 7 行目:
datatype
が"dict"
でも、"tuple"
でもない 場合は「不正(invalid)なデータ型」という意味の エラーメッセージを表示 してNone
を返す
略
1 if datatype == "dict":
2 return count
3 elif datatype == "tuple":
4 return count[Marubatsu.CIRCLE], count[Marubatsu.CROSS], count[Marubatsu.EMPTY]
5 else:
6 print("invalid datatype", datatype)
7 return None
count_marks
の互換性の確認
count_marks
が 互換性を持つ ようになったので、今度は下記のプログラムを実行しても エラー が 発生しなくなり、実行結果 から ai6s
が 正しく動作 することも 確認 できます。
ai_match(ai=[ai6s, ai6s])
実行結果(実行結果はランダムなので下記とは異なる場合があります)
ai6s VS ai6s
count win lose draw
o 3060 1726 5214
x 1761 3100 5139
total 4821 4826 10353
ratio win lose draw
o 30.6% 17.3% 52.1%
x 17.6% 31.0% 51.4%
total 24.1% 24.1% 51.8%
enum_markpats
の修正と確認
count_marks
の 仕様に合わせて、enum_markpats
を下記のように 修正 します。
-
6、8、11、14 行目:
count_marks
に キーワード引数datatype="tuple"
を 記述 する3
1 def enum_markpats(self):
2 markpats = set()
3
4 # 横方向と縦方向の判定
5 for i in range(self.BOARD_SIZE):
6 count = self.count_marks(coord=[0, i], dx=1, dy=0, datatype="tuple")
7 markpats.add(count)
8 count = self.count_marks(coord=[i, 0], dx=0, dy=1, datatype="tuple")
9 markpats.add(count)
10 # 左上から右下方向の判定
11 count = self.count_marks(coord=[0, 0], dx=1, dy=1, datatype="tuple")
12 markpats.add(count)
13 # 右上から左下方向の判定
14 count = self.count_marks(coord=[2, 0], dx=-1, dy=1, datatype="tuple")
15 markpats.add(count)
16
17 return markpats
18
19 Marubatsu.enum_markpats = enum_markpats
行番号のないプログラム
def enum_markpats(self):
markpats = set()
# 横方向と縦方向の判定
for i in range(self.BOARD_SIZE):
count = self.count_marks(coord=[0, i], dx=1, dy=0, datatype="tuple")
markpats.add(count)
count = self.count_marks(coord=[i, 0], dx=0, dy=1, datatype="tuple")
markpats.add(count)
# 左上から右下方向の判定
count = self.count_marks(coord=[0, 0], dx=1, dy=1, datatype="tuple")
markpats.add(count)
# 右上から左下方向の判定
count = self.count_marks(coord=[2, 0], dx=-1, dy=1, datatype="tuple")
markpats.add(count)
return markpats
Marubatsu.enum_markpats = enum_markpats
修正箇所
def enum_markpats(self):
markpats = set()
# 横方向と縦方向の判定
for i in range(self.BOARD_SIZE):
- count = self.count_marks(coord=[0, i], dx=1, dy=0)
+ count = self.count_marks(coord=[0, i], dx=1, dy=0, datatype="tuple")
markpats.add(count)
- count = self.count_marks(coord=[i, 0], dx=0, dy=1)
+ count = self.count_marks(coord=[i, 0], dx=0, dy=1, datatype="tuple")
markpats.add(count)
# 左上から右下方向の判定
- count = self.count_marks(coord=[0, 0], dx=1, dy=1)
+ count = self.count_marks(coord=[0, 0], dx=1, dy=1, datatype="tuple")
markpats.add(count)
# 右上から左下方向の判定
- count = self.count_marks(coord=[2, 0], dx=-1, dy=1)
+ count = self.count_marks(coord=[2, 0], dx=-1, dy=1, datatype="tuple")
markpats.add(count)
return markpats
Marubatsu.enum_markpats = enum_markpats
次に、修正 した enum_markpats
が 正しく動作するか を、先程のプログラムで 確認 します。実行結果は先ほどと同じなので省略しますが、正しく動作 することが 確認 できます。
mb = Marubatsu()
print(mb)
pprint(mb.enum_markpats())
mb.move(1, 1)
print(mb)
pprint(mb.enum_markpats())
mb.move(0, 0)
print(mb)
pprint(mb.enum_markpats())
mb.move(1, 0)
print(mb)
pprint(mb.enum_markpats())
ai8s
の修正方法 その 1
enum_markpats
が 返す値 が、dict を要素 とする list から、tuple を要素 とする list に 変更 されたので、それに合わせて ai8s
の下記のプログラムの 5、8 行目 の 条件式 の部分を 修正 する 必要 があります。どのように修正すれば良いかについて少し考えてみて下さい。
1 def ai8s(mb, debug=False):
2 def eval_func(mb):
略
3 markpats = mb.enum_markpats()
4 # 相手が勝利できる場合は評価値として -1 を返す
5 if {mb.turn: 2, Marubatsu.EMPTY: 1} in markpats:
6 return -1
7 # 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
8 elif {mb.last_turn: 2, Marubatsu.EMPTY: 1} in markpats:
9 return 1
10 # それ以外の場合は評価値として 0 を返す
11 else:
12 return 0
略
まず、2 つある条件式のうち、5 行目 の下記の 条件式 を 修正 する 方法 を 考えます。
if {mb.turn: 2, Marubatsu.EMPTY: 1} in markpats:
return -1
上記は、「自 0 敵 2 空 1」という、敵が勝利できる マークのパターン を表す dict が markpats
の 要素に存在するか どうかを 判定 しています。従って、これを、そのマークのパターン を表す tuple に変更 する必要があります。
enum_markpats
が 返す、マークのパターン の 一覧 の中の tuple の要素には、「〇、×、空」の 順 でマークの数が代入されます。従って、「自 0 敵 2 空 1」を表す tuple は、自分の手番 の 種類によって、下記のように 異なります。
-
自分の手番 が 〇 の場合は、「自 0 敵 2 空 1」は、「〇 0 × 2 空 1」なので、 このマークのパターン を表す tuple は
(0, 2, 1)
である -
自分の手番 が × の場合は、「自 0 敵 2 空 1」は、「〇 2 × 0 空 1」なので、このマークのパターン を表す tuple は
(2, 0, 1)
である
下記は、自分の手番 が 〇 と × の 場合に分けて 上記の 条件式を修正 したプログラムです。なお、mb
は 相手の手番 の 局面 なので、自分の手番 が 〇 であるか どうかは、mb.turn == Marubatsu.CROSS
で 判定 する 必要がある 点に 注意 して下さい。
# 相手が勝利できる場合は評価値として -1 を返す
if mb.turn == Marubatsu.CROSS and (0, 2, 1) in markpats:
return -1
if mb.turn == Marubatsu.CIRCLE and (2, 0, 1) in markpats:
return -1
また、or 演算子 を 利用 することで、下記のプログラムのように 1 つの if 文 に まとめる こともできます。or 演算子 のほうが、and 演算子より も 優先順位 が 低い ので、1 つの if 文 に まとめる際 に、元の条件式 を ()
で囲う必要 がある点に 注意 が必要です。
# 相手が勝利できる場合は評価値として -1 を返す
if (mb.turn == Marubatsu.CROSS and (0, 2, 1) in markpats) or \
(mb.turn == Marubatsu.CIRCLE and (2, 0, 1) in markpats):
return -1
ai8s
の修正
もう一つ の 条件式 も 同様の方法 で 記述 できるので、ai8s
を下記のプログラムのように 修正 します。
from ai import ai_by_score
def ai8s(mb, debug=False):
def eval_func(mb):
# 真ん中のマスに着手している場合は、評価値として 2 を返す
if mb.last_move == (1, 1):
return 3
# 自分が勝利している場合は、評価値として 1 を返す
if mb.status == mb.last_turn:
return 2
markpats = mb.enum_markpats()
# 相手が勝利できる場合は評価値として -1 を返す
if (mb.turn == Marubatsu.CROSS and (0, 2, 1) in markpats) or \
(mb.turn == Marubatsu.CIRCLE and (2, 0, 1) in markpats):
return -1
# 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
elif (mb.turn == Marubatsu.CROSS and (2, 0, 1) in markpats) or \
(mb.turn == Marubatsu.CIRCLE and (0, 2, 1) in markpats):
return 1
# それ以外の場合は評価値として 0 を返す
else:
return 0
return ai_by_score(mb, eval_func, debug=debug)
修正箇所
from ai import ai_by_score
def ai8s(mb, debug=False):
def eval_func(mb):
# 真ん中のマスに着手している場合は、評価値として 2 を返す
if mb.last_move == (1, 1):
return 3
# 自分が勝利している場合は、評価値として 1 を返す
if mb.status == mb.last_turn:
return 2
markpats = mb.enum_markpats()
# 相手が勝利できる場合は評価値として -1 を返す
- if {mb.turn: 2, Marubatsu.EMPTY: 1} in markpats:
+ if (mb.turn == Marubatsu.CROSS and (0, 2, 1) in markpats) or \
+ (mb.turn == Marubatsu.CIRCLE and (2, 0, 1) in markpats):
return -1
# 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
- elif {mb.last_turn: 2, Marubatsu.EMPTY: 1} in markpats:
+ elif (mb.turn == Marubatsu.CROSS and (2, 0, 1) in markpats) or \
+ (mb.turn == Marubatsu.CIRCLE and (0, 2, 1) in markpats):
return 1
# それ以外の場合は評価値として 0 を返す
else:
return 0
return ai_by_score(mb, eval_func, debug=debug)
動作の確認
前回の記事で、enum_markpats
を 実装 した際に ai8s2
という、ai8s
とは 異なる名前 で ルール 8 の AI を定義 したので、今回 は何故 そのようにしないのか と 思った人がいるかも しれません。前回の記事で ai8s
と 異なる名前 で AI を定義 したのは、enum_markpats
を 利用するように修正 した AI が 正しく動作するか どうかを、enum_markpats
を 利用しない修正前 の ai8s
と 対戦して確認 できるように するため でした。一方、今回の場合 は、修正前 の ai8s
も enum_markpats
を 利用しています。そのため、元の ai8s
そのものが 動作しなく なっており、ai8s
を 残しておいても意味がない からです。従って、修正した ai8s
が 正しく動作するか どうかは、別の方法 で 確認 する 必要 があります。
その方法は、ai8s
VS ai2
と ai8s
VS ai7s
の対戦を行い、以前の記事 で行った 同じ組み合わせ の 対戦結果 と 比較 して 成績 が ほぼ変わらない ことを 確認する という方法です。
下記は ai2
との対戦です。
ai_match(ai=[ai8s, ai2])
実行結果(実行結果はランダムなので下記とは異なる場合があります)
ai8s VS ai2
count win lose draw
o 9847 10 143
x 8934 246 820
total 18781 256 963
ratio win lose draw
o 98.5% 0.1% 1.4%
x 89.3% 2.5% 8.2%
total 93.9% 1.3% 4.8%
下記は ai7s
との対戦です。
ai_match(ai=[ai8s, ai7s])
実行結果(実行結果はランダムなので下記とは異なる場合があります)
ai8s VS ai7s
count win lose draw
o 3365 384 6251
x 378 2871 6751
total 3743 3255 13002
ratio win lose draw
o 33.7% 3.8% 62.5%
x 3.8% 28.7% 67.5%
total 18.7% 16.3% 65.0%
下記は、前回 と 今回 の 対戦結果 の 表 です。いずれも前回と今回で 成績 は ほぼ変わらない ので、正しく修正 された 可能性が高い ことが 確認 できました。
関数名 | o 勝 | o 負 | o 分 | x 勝 | x 負 | x 分 | 勝 | 負 | 分 |
---|---|---|---|---|---|---|---|---|---|
前回の VS ai2
|
98.2 | 0.1 | 1.6 | 89.4 | 2.5 | 8.1 | 93.8 | 1.3 | 4.9 |
今回の VS ai2
|
98.5 | 0.1 | 1.4 | 89.3 | 2.5 | 8.2 | 93.9 | 1.3 | 4.8 |
前回の VS ai7s
|
33.4 | 3.9 | 62.6 | 3.9 | 28.8 | 67.3 | 18.7 | 16.4 | 65.0 |
今回の VS ai7s
|
33.7 | 3.8 | 62.5 | 3.8 | 28.7 | 67.5 | 18.7 | 16.3 | 65.8 |
ai8s
の修正方法 その 2
先程 の ai8s
は、自分の手番 によって 異なる tuple で 判定を行う必要 があるため、プログラム が 長く、分かりづらくなる という 欠点 があります。その 原因 は、count_marks
が返す tuple の 要素 が「〇 × 空」のように マークを基準 に 数えている のに対し、判定 を 行う際 の マークのパターン では「自 敵 空」のように、マークではなく、誰の手番 であるかを 基準 に 数えている からです。そのため、この欠点は、count_marks
が返す tuple の要素 を、誰の手番 であるかを 基準 に 数える ように 修正 することで 解消 できます。
count_marks
の修正
count_marks
を下記のプログラムのように 修正 します。
- 12 行目:直前の手番 のマーク、現在の手番 のマーク、空のマス の 順 で 数えた数 を 要素 として持つ tuple を返す ように 修正 する
「直前の手番 のマーク、現在の手番 のマーク、空のマス」の 順番 は、上記以外 の順番にしても かまいません が、その場合はこの後で ai8s
のプログラムを、その順番 に 合わせて修正 する 必要 があります。
1 def count_marks(self, coord, dx, dy, datatype="dict"):
2 x, y = coord
3 count = defaultdict(int)
4 for _ in range(self.BOARD_SIZE):
5 count[self.board[x][y]] += 1
6 x += dx
7 y += dy
8
9 if datatype == "dict":
10 return count
11 else:
12 return count[self.last_turn], count[self.turn], count[Marubatsu.EMPTY]
13
14 Marubatsu.count_marks = count_marks
行番号のないプログラム
def count_marks(self, coord, dx, dy, datatype="dict"):
x, y = coord
count = defaultdict(int)
for _ in range(self.BOARD_SIZE):
count[self.board[x][y]] += 1
x += dx
y += dy
if datatype == "dict":
return count
else:
return count[self.last_turn], count[self.turn], count[Marubatsu.EMPTY]
Marubatsu.count_marks = count_marks
修正箇所
def count_marks(self, coord, dx, dy, datatype="dict"):
x, y = coord
count = defaultdict(int)
for _ in range(self.BOARD_SIZE):
count[self.board[x][y]] += 1
x += dx
y += dy
if datatype == defaultdict:
return count
else:
- return count[Marubatsu.CIRCLE], count[Marubatsu.CROSS], count[Marubatsu.EMPTY]
+ return count[self.last_turn], count[self.turn], count[Marubatsu.EMPTY]
Marubatsu.count_marks = count_marks
ai8s
の修正
上記の修正 を行うことで、「自 x 敵 y 空 z」という マークのパターン を、常に (x, y, z)
という tuple で 表現 できるようになります。従って、ai8s
は、下記のプログラムのように、手番 に 関わらず、同じ条件式 で 簡潔に記述 できます。なお、mb
は 相手の手番 の 局面 なので、自分の手番 は、mb
の 直前の手番 で表される点に 注意 して下さい。
- 13 行目:「自 0 敵 2 空 1」は、常に
(0, 2, 1)
という tuple で表現 できる - 16 行目:「自 2 敵 0 空 1」は、常に
(2, 0, 1)
という tuple で表現 できる
1 def ai8s(mb, debug=False):
2 def eval_func(mb):
3 # 真ん中のマスに着手している場合は、評価値として 2 を返す
4 if mb.last_move == (1, 1):
5 return 3
6
7 # 自分が勝利している場合は、評価値として 1 を返す
8 if mb.status == mb.last_turn:
9 return 2
10
11 markpats = mb.enum_markpats()
12 # 相手が勝利できる場合は評価値として -1 を返す
13 if (0, 2, 1) in markpats:
14 return -1
15 # 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
16 elif (2, 0, 1) in markpats:
17 return 1
18 # それ以外の場合は評価値として 0 を返す
19 else:
20 return 0
21
22 return ai_by_score(mb, eval_func, debug=debug)
行番号のないプログラム
def ai8s(mb, debug=False):
def eval_func(mb):
# 真ん中のマスに着手している場合は、評価値として 2 を返す
if mb.last_move == (1, 1):
return 3
# 自分が勝利している場合は、評価値として 1 を返す
if mb.status == mb.last_turn:
return 2
markpats = mb.enum_markpats()
# 相手が勝利できる場合は評価値として -1 を返す
if (0, 2, 1) in markpats:
return -1
# 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
elif (2, 0, 1) in markpats:
return 1
# それ以外の場合は評価値として 0 を返す
else:
return 0
return ai_by_score(mb, eval_func, debug=debug)
修正箇所
def ai8s(mb, debug=False):
def eval_func(mb):
# 真ん中のマスに着手している場合は、評価値として 2 を返す
if mb.last_move == (1, 1):
return 3
# 自分が勝利している場合は、評価値として 1 を返す
if mb.status == mb.last_turn:
return 2
markpats = mb.enum_markpats()
# 相手が勝利できる場合は評価値として -1 を返す
- if (mb.turn == Marubatsu.CROSS and (0, 2, 1) in markpats) or \
- (mb.turn == Marubatsu.CIRCLE and (2, 0, 1) in markpats):
+ if (0, 2, 1) in markpats:
return -1
# 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
- elif (mb.turn == Marubatsu.CROSS and (2, 0, 1) in markpats) or \
- (mb.turn == Marubatsu.CIRCLE and (0, 2, 1) in markpats):
+ elif (2, 0, 1) in markpats:
return 1
# それ以外の場合は評価値として 0 を返す
else:
return 0
return ai_by_score(mb, eval_func, debug=debug)
動作の確認
先程と同様 に、ai2
、ai7s
と 対戦 することで、正しく修正されているか どうかを 確認 します。まず、ai2
と 対戦 します。
ai_match(ai=[ai8s, ai2])
実行結果(実行結果はランダムなので下記とは異なる場合があります)
ai8s VS ai2
count win lose draw
o 9832 11 157
x 8895 245 860
total 18727 256 1017
ratio win lose draw
o 98.3% 0.1% 1.6%
x 88.9% 2.5% 8.6%
total 93.6% 1.3% 5.1%
次に、ai7s
と 対戦 します。
ai_match(ai=[ai8s, ai7s])
実行結果(実行結果はランダムなので下記とは異なる場合があります)
ai8s VS ai7s
count win lose draw
o 3355 433 6212
x 366 2904 6730
total 3721 3337 12942
ratio win lose draw
o 33.6% 4.3% 62.1%
x 3.7% 29.0% 67.3%
total 18.6% 16.7% 64.7%
下記は、前回 と 今回 の 対戦結果 の 表 です。いずれも前回と今回で 成績 は ほぼ変わらない ので、正しく修正 された 可能性が高い ことが 確認 できました。
関数名 | o 勝 | o 負 | o 分 | x 勝 | x 負 | x 分 | 勝 | 負 | 分 |
---|---|---|---|---|---|---|---|---|---|
前回の VS ai2
|
98.2 | 0.1 | 1.6 | 89.4 | 2.5 | 8.1 | 93.8 | 1.3 | 4.9 |
今回の VS ai2
|
98.3 | 0.1 | 1.6 | 88.9 | 2.5 | 8.6 | 93.6 | 1.3 | 5.1 |
前回の VS ai7s
|
33.4 | 3.9 | 62.6 | 3.9 | 28.8 | 67.3 | 18.7 | 16.4 | 65.0 |
今回の VS ai7s
|
33.6 | 4.3 | 62.1 | 3.7 | 29.0 | 67.3 | 18.6 | 16.7 | 64.7 |
ai8s
の処理速度の検証
前回の記事で list を使って ai8s
を 実装 した場合と、set を使って 実装 した場合の 処理時間 を 比較 すると説明しました。先程 set で ai8s
を 実装 したので、実際に 比較 します。
筆者のパソコン では、下記のプログラムで ai8s
どうし を 対戦 すると 約 47.2 秒 かかりました。以前の記事で list を使って実装した ai8s2
どうし の対戦では、約 45.5 秒 かかったので、list と set で ほとんど処理速度 が 変わらない ことが分かります。
ai_match(ai=[ai8s, ai8s])
処理速度 がほとんど 変わらない原因 は、ai8s
では、list や set の 要素の数 が 最大で 8 であり、前回の記事で説明したように、list や set の 要素の数 が 10 前後 のような 少ない場合 は、処理速度 に 差 が ほとんどない からだと思われます。
今回の記事のまとめ
今回の記事では、dict を ハッシュ可能なオブジェクト に 変換する方法 として、tuple に変換 する方法について説明しました。
また、互換性 という 概念を説明 し、関数 や モジュール の 仕様を修正 した際の、互換性の問題 への いくつかの対処法 について 説明 しました。
次回の記事では、dict を tuple 以外 の ハッシュ可能なオブジェクト に 変換する方法 などについて説明します。
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
以下のリンクは、今回の記事で更新した marubatsu.py です。
以下のリンクは、今回の記事で更新した ai.py です。
次回の記事