LoginSignup
0
0

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

Last updated at Posted at 2024-02-11

目次と前回の記事

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

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

これまでに作成した 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 があるので、まず defaultdicttuple に変換 する 方法考える ことにします。

defaultdict は、既定値 に関する処理 以外 では dict同じ性質 を持ちます。今回の記事で紹介する、defaultdicttuple に変換 する 方法 では、既定値関する処理行わない ので、dictdefaultdict は、同じ方法tuple に変換 できます。

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

count_marks の修正

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

  • 11 行目count から Marubatsu.CIRCLEMarubatsu.CROSSMarubatsu.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]計算 する際に、上記エラーが発生 します。

この エラー発生 するようになった 原因 は、ai6scount_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 に対して 完全互換性を持つ
  • CB に対して 完全互換性を持つ
  • CD に対して 完全互換性を持つ
  • A、B、C、D」は E に対して 完全互換性を持つ

XY を Python の set(集合)として 考える と、X が Y に対して完全互換性を持つ 場合は、X >= YTrue になります。

上位互換(upper compatible)

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

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

  • B、C、D」は A に対して 上位互換性を持つ
  • CB に対して 上位互換性を持つ
  • CD に対して 上位互換性を持つ
  • B、C、D」は E に対して 上位互換性を持つ

XY を Python の set(集合)として 考える と、X が Y に対して上位互換性を持つ 場合は、X > YTrue になります。

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

下位互換(lower compatible)

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

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

  • A、E」は B に対して 下位互換性を持つ
  • A、B、D、E」は C に対して 下位互換性を持つ
  • A、E」は D に対して 下位互換性を持つ

時計 B時計 D は、お互いが持たない機能持つ ので、下位互換 ではありません。

XY を Python の set(集合)として 考える と、X が Y に対して下位互換性を持つ 場合は、X < YTrue になります。

後方互換(backward compatible)

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

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

  • B、C、D、E」は A に対して 後方互換性を持つ
  • C、D、E」は B に対して 後方互換性を持つ
  • D、E」は C に対して 後方互換性を持つ
  • ED に対して 後方互換性を持つ

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

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

前方互換(forward compatible)

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

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

  • AB に対して 前方互換性を持つ
  • A、B」は C に対して 前方互換性を持つ
  • A、B、C」は D に対して 前方互換性を持つ
  • A、B、C、D」は E に対して 前方互換性を持つ

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

非互換(incompatible)

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

個別の性質の互換性

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

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

バージョンと互換性

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

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

例えば、ソニーのプレーステーション(PlayStation。以後は PS と表記する)というゲーム機は PS1PS2PS3PS4PS5 という 順番 でバージョンが付けられています。

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.pyai.py などの モジュール毎回の記事更新 し、Github記事の番号 と同じ 053 のような名前の フォルダモジュールファイルを保存 しています。本記事では モジュールバージョン をつけて 区別していません が、フォルダ名番号マイナーバージョンみなす ことが できます

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

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

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

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

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

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

互換性の確認方法

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

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

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

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

Python に限らず、プログラムモジュール一般に公開 する際に、その プログラムを作成 した プログラム言語バージョン や、モジュールバージョン などを 明記 する 場合が多い のは、互換性原因 です。本記事でも、初回の記事 でそれらを 明記 しています。

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

互換性 は、ソフトウェア だけでなく、ハードウェア にも 存在 します。例えばプレーステーション(PS)というゲーム機は、PS1PS2 の間で 互換性があった ため、PS2PS1 のソフトを そのまま遊ぶ ことが できました が、PS3PS4 の間には 互換性がない ので、PS4PS3 のソフトをそのまま 遊ぶ ことは できません

プログラムの後方互換性

プログラムでは、後方互換性特に重要 です。例えば 関数モジュール仕様を更新 して バージョンアップ した際に、新しいバージョンが 後方互換性 かつ 完全互換性 を持つ場合は、古いバージョン を使って作られた プログラム変更することなくそのまま利用 することが できます上記のノート で紹介した、PS2PS1 のゲームそのまま遊べる のは、PS2PS1 に対して 後方互換性完全互換性 を持つからです。一方、今回の記事で バージョンアップ した 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_tupletupleマークのパターン返す メソッドですが、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 行目dicttuple変換 した値を 返す
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 行目そうでない 場合に、counttuple に変換 した値を 返す
 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」という、敵が勝利できる マークのパターン を表す dictmarkpats要素に存在するか どうかを 判定 しています。従って、これを、そのマークのパターン を表す 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 とは 異なる名前ルール 8AI を定義 したので、今回 は何故 そのようにしないのか思った人がいるかも しれません。前回の記事で ai8s異なる名前AI を定義 したのは、enum_markpats利用するように修正 した AI正しく動作するか どうかを、enum_markpats利用しない修正前ai8s対戦して確認 できるように するため でした。一方、今回の場合 は、修正前ai8senum_markpats利用しています。そのため、元の ai8s そのものが 動作しなく なっており、ai8s残しておいても意味がない からです。従って、修正した ai8s正しく動作するか どうかは、別の方法確認 する 必要 があります。

その方法は、ai8s VS ai2ai8s 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) 

動作の確認

先程と同様 に、ai2ai7s対戦 することで、正しく修正されているか どうかを 確認 します。まず、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 を使って 実装 した場合の 処理時間比較 すると説明しました。先程 setai8s実装 したので、実際に 比較 します。

筆者のパソコン では、下記のプログラムで ai8s どうし対戦 すると 約 47.2 秒 かかりました。以前の記事list を使って実装した ai8s2 どうし の対戦では、約 45.5 秒 かかったので、listsetほとんど処理速度変わらない ことが分かります。

ai_match(ai=[ai8s, ai8s])

処理速度 がほとんど 変わらない原因 は、ai8s では、listset要素の数最大で 8 であり、前回の記事で説明したように、listset要素の数10 前後 のような 少ない場合 は、処理速度ほとんどない からだと思われます。

今回の記事のまとめ

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

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

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

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

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

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

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

次回の記事

  1. indices は、index(インデックス)の 複数形 です

  2. Office 365 という、クラウドの Office 製品は 例外 です。クラウドと 365 の意味を説明すると長くなるので省略しますが、興味がある方は調べてみると良いでしょう

  3. 実際には、"dict" 以外 であれば、キーワード引数の datatype に代入する値に 何を記述しても構いません が、分かりやすさ重視 して "tuple" を記述 しました

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0