0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-02-15

目次と前回の記事

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

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

これまでに作成した 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要素意味 は、順番 に「直前の手番 のマーク、現在の手番 のマーク、空のマス」の数のように決めましたが、tupledict のよう に、それぞれの 要素の意味表現 できる キー持たない ので、例えば (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 は、以下のような、tupledict性質併せ持つ データ型です。

  • 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サブクラスを定義 する 必要 があります。

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

サブクラスとは何か

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

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

namedtuple利用 する際には、多くの場合同じ意味 を持つ namedtupleデータ複数作成 します。例えば、count_marksnamedtuple返す ように 修正 した場合、「直線の手番 のマーク、現在の手番 のマーク、空のマス」の数を 要素 として持つ namedtuplecount_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)を表す 文字列先頭の要素 から 順番 に持つ、listtuple などの シーケンス型 で記述します。namedtuple では、すべての要素名前を付ける必要 があるので、filed_names要素の数 が、カスタムデータ型要素の数一致 します。なお、フィールド名 として、それぞれの 要素の名前利用できるデータ には、以下のような 制限 があります。

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

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

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

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

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

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

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

下記のプログラムのように、typenamefield_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 から「直前の手番現在の手番空のマス」に 対応する要素 に「201」を 代入 した データ(インスタンス)を 作成 するプログラムです。キーワード引数記述 した場合は、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記述 することで、それぞれ要素デフォルト値設定 することが できます。例えば、下記のプログラムは、xy という 名前 が付けられた 要素 に、それぞれ 12 という デフォルト値設定 された namedtupleカスタムデータ型定義 します。なお、この カスタムデータ型 は、2 次元点(point)の座標 のように 扱うことができる ので typenamePoint という 名前 を付けました。

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" にそれぞれ 12 という デフォルト値設定 されます。なお、この カスタムデータ型 は、3 次元(3D)の 座標 のように 扱うことができる ので typenamePoint3D という 名前 を付けました。

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利用方法 について説明します。

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

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

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

実行結果

2 0 1

要素の展開

namedtupletuple性質を持つ ので、下記のプログラムのように、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異なり、下記のプログラムのように、[]要素の名前 を使って 要素参照 しようとすると エラーが発生 します。これは、エラーメッセージからわかるように namedtupletuple一種 なので、[] によって 指定できる のは インデックス だからです。namedtupledict混同しない ように 注意 して下さい。

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ブロックの中 でも 利用 します。そのため、Markpatmarubatsu.py の中定義 する場合は、ai.py などの 他のモジュール から インポートできる ようにする 必要 があります。そのためには、MarkpatMarubatsu クラスの ブロックの外定義 する 必要 があります。

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

count_marks の修正

次に、下記のように、count_marksMarkpat利用 するように 修正 します。

  • 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変更することなく そのまま 実行 することが できます

前回の記事同様 に、ai2ai7s対戦 し、以前の対戦結果比較 することで、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) のほうが 簡潔に記述できる という 利点 があるので、tuplenamedtupleどちらを利用 したほうが 良いか は、人によって判断が異なる のではないかと思います。本記事 では 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 行目namedtupleMarkpat(0, 2, 1) のように 記述 することも できますが、わざわざ namedtuple利用 する 意味がなくなります

動作の確認

先程と 同様 に、ai2ai7s対戦 し、以前の対戦結果比較 することで、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 モジュール定義 されています。NamedTuplenamedtuple違い以下3 点 で、それ以外機能は同じ です。

  • NamedTuple では、それぞれの 要素型アノテーション指定できる
  • カスタムデータ型(tuple のサブクラス)の 定義方法異なる
  • docstring記述できる

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

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

NamedTuple のインポート

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

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)

動作の確認

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

先程と 同様 に、ai2ai7s対戦 し、以前の対戦結果比較 することで、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デフォルト値設定方法説明 した際に 定義 した Point3DNamedTuple定義 するプログラムです。実行結果 は、先ほど全く同じ になります。

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 が用意されているので、この機会に説明します。興味がない方は読み飛ばしてもらっても構いません。

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

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

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

dataclass を使わない実装方法

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

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

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

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

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 のインポート

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

from dataclasses import dataclass

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

dataclass利用 した クラスの定義 は、NamedTupleほぼ同様方法記述 します。NamedTuple との 違い は、以下の 2 点 です。

  • クラスの定義直前の行@dataclass記述する
  • クラスの名前直後(NamedTuple)記述しない

@dataclass は、以前の記事で、クラスメソッド定義 する際に クラスの定義直前記述 した @classmethod同様 の、デコレータ と呼ばれるもので、関数やメソッド に対して何らかの 機能を付け加える(飾り付ける(decorate))という 機能 を持ちます。

下記は、先程の Pointdataclass を使って 定義 するプログラムです。下記では記述していませんが、通常のクラス同様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 になります。

  • 同じ属性の値 を持つ、p1p2作成 する
  • p1p2z という 属性別の値代入 する
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__ メソッドが 実行 されます。従って、下記のプログラムで、これまで のように、xy 属性に 12代入 された 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__ メソッドは 実行されない ため、インスタンスxy 属性に 代入されません。そのため、下記のプログラムを実行すると、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 の違い

dataclassNamedTuple似ています が、下記の点異なる ので、使い分ける 必要があります。

ミュータブルである

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 を利用した ai6sai7s の定義

enum_markpats を利用しない実装方法の例として残しておきたいので、本記事では採用しませんが、ai6sai7senum_markpats利用 して 簡潔に記述 することが できます。下記は、ai6senum_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%

下記は、ai7senum_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ハッシュ可能なオブジェクト変換 する方法として、namedtupleNamedTuple利用する方法 について説明しました。

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

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

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

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

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

次回の記事

  1. 特定のクラス性質受け継いだサブクラス定義 することを、クラスの継承 と呼びます。クラスの継承については今後の記事で紹介します

  2. 後述の、デフォルト値設定 した場合を 除きます

  3. __new____init__似た性質 を持つ 特殊メソッド で、インスタンス作成された際自動的呼び出されます__new____init__ の違いは複雑なので説明は省略します。興味がある方は、こちらの リンク 先を参照して下さい

  4. デフォルト値位置引数記述 することは できません

  5. この部分の意味 については、NamedTuple利用する際理解する必要ありません ので今回の記事では説明を省略します。なお、この部分 は関数の仮引数のように見えるかもしれませんが、仮引数ではありません。この部分の意味については今後の記事で説明します

  6. 下記の場合は、正確には クラス属性 です

  7. post は、英語で ~の後の という 意味 を持ちます

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?