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?

More than 1 year has passed since last update.

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

0
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 の 要素 の 意味 は、順番 に「直前の手番 のマーク、現在の手番 のマーク、空のマス」の数のように決めましたが、tuple は dict のよう に、それぞれの 要素の意味 を 表現 できる キー を 持たない ので、例えば (2, 0, 1) という tuple を 見ただけ では、それぞれの 要素 が 何を意味する のかが わからない という 欠点 があります。

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

namedtuple の性質と使い方

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

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

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

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

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

namedtuple の性質

namedtuple は、以下のような、tuple と dict の 性質 を 併せ持つ データ型です。

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

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

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

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

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

namedtuple のインポート

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

from collections import namedtuple

namedtuple のデータの作成手順

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

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

サブクラスとは何か

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

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

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

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

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

以前の記事で説明したように、クラス を 自分で定義 する事で、任意のデータ構造 を 管理 し、そのデータを 処理 する メソッド を持つ カスタムデータ型 を定義することができます。また、定義した クラス から インスタンスを作成 することで、同じデータ構造 と 機能 をもつ データ を いくらでも作る ことが できるようになります。実際に Marubatsu クラスを定義する事で、〇×ゲーム の 機能 を持つ データ を いくつも作成 してきました。

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

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

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

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

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

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

変数名 = namedtuple(typename, field_names)

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

field_names には、namedtuple の カスタムデータ型 の フィールド名(field names)を表す 文字列 を 先頭の要素 から 順番 に持つ、list や tuple などの シーケンス型 で記述します。namedtuple では、すべての要素 に 名前を付ける必要 があるので、filed_names の 要素の数 が、カスタムデータ型 の 要素の数 に 一致 します。なお、フィールド名 として、それぞれの 要素の名前 に 利用できるデータ には、以下のような 制限 があります。

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

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

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

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

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

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

  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")

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

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

実行結果

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

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

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

  • ValueError
    値(Value)に関するエラー
  • Type names and field names must be valid identifiers: '0Markpat'
    '0Markpat' という、データ型(type)や(and)フィールド(field)の名前(names)は、有効(valid)な識別子(identifiers)でなければならない
# フィールド名が _ で始まっている
namedtuple("Markpat", ["_last_turn", "turn", "empty"]) 

実行結果

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

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

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

  • ValueError
    値(Value)に関するエラー
  • ValueError: Field names cannot start with an underscore: '_last_turn'
    '_last_turn' というフィールド名(field names)は _(underscore)で始める(start with)ことはできない(can not)

namedtuple のインスタンスの作成

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

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

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

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

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

実行結果

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

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

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

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

実行結果

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

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

print(Markpat(0, 1))

実行結果

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

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

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

  • TypeError
    データ型(type)に関するエラー
  • Markpat.new() missing 1 required positional argument: 'empty'
    Markpat からインスタンスを作成する際に呼び出される __new__3 メソッドで必要とされる(required) 仮引数 empty に対応する位置引数(positional argument)が存在しない(missing)

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

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

実行結果

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

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

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

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

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

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

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

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

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

実行結果

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

defaults に 記述 された 要素の数 が、field_names に 記述 された 要素の数より も 少ない 場合は、field_names の 先頭 から 足りない分 の数の 要素 には デフォルト値 は 設定されません。これは、以前の記事で説明したように、デフォルト引数 は、通常の仮引数より も 後に記述 する 必要 があるからです。

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

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

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

実行結果

Point3D(x=3, y=1, z=2)
Point3D(x=4, y=5, z=2)
Point3D(x=6, y=7, z=8)

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

print(Point3D())

実行結果

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

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

namedtuple の利用方法

表記が長いので、以後は namedtuple の カスタムデータ型 から作成した データ のことを、list 等と 同様 に、単に nametuple と 表記 します。

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

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

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

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

実行結果

2 0 1

要素の展開

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

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

実行結果

2 0 1

名前による要素の参照

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

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

実行結果

2 0 1

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

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

実行結果

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

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

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

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

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

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

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

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

実行結果

2

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

namedtuple を利用した ai8s の修正

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

Markpat の定義と保存場所

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

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

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

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

count_marks の修正

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

  • 15 行目:Markpat の インスタンス を 作成 して 返す ように 修正 する
 1  from marubatsu import Marubatsu
 2  from collections import defaultdict
 3
 4  def count_marks(self, coord, dx, dy, datatype="dict"):     
 5      x, y = coord   
 6      count = defaultdict(int)
 7      for _ in range(self.BOARD_SIZE):
 8          count[self.board[x][y]] += 1
 9          x += dx
10          y += dy
11
12      if datatype == "dict":
13          return count
14      else:
15          return Markpat(count[self.last_turn], count[self.turn], count[Marubatsu.EMPTY])
16
17  Marubatsu.count_marks = count_marks
行番号のないプログラム
from marubatsu import Marubatsu
from collections import defaultdict

def count_marks(self, coord, dx, dy, datatype="dict"):     
    x, y = coord   
    count = defaultdict(int)
    for _ in range(self.BOARD_SIZE):
        count[self.board[x][y]] += 1
        x += dx
        y += dy

    if datatype == "dict":
        return count
    else:
        return Markpat(count[self.last_turn], count[self.turn], count[Marubatsu.EMPTY])

Marubatsu.count_marks = count_marks
修正箇所
from marubatsu import Marubatsu
from collections import defaultdict

def count_marks(self, coord, dx, dy, datatype="dict"):     
    x, y = coord   
    count = defaultdict(int)
    for _ in range(self.BOARD_SIZE):
        count[self.board[x][y]] += 1
        x += dx
        y += dy

    if datatype == "dict":
        return count
    else:
-       return count[self.last_turn], count[self.turn], count[Marubatsu.EMPTY]
+       return Markpat(count[self.last_turn], count[self.turn], count[Marubatsu.EMPTY])

Marubatsu.count_marks = count_marks

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

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

動作の確認

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

前回の記事と 同様 に、ai2、ai7s と 対戦 し、以前の対戦結果と 比較 することで、ai8s が 正しく動作するか どうかを 確認 します。まず、ai2 と対戦します。

from ai import ai_match, ai2, ai7s, ai8s

ai_match(ai=[ai8s, ai2])

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

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

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

次に、ai7s と対戦します。

ai_match(ai=[ai8s, ai7s])

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

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

ratio     win    lose    draw
o       33.1%    3.9%   62.9%
x        3.8%   29.2%   67.0%
total   18.4%   16.6%   65.0%

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

関数名 o 勝 o 負 o 分 x 勝 x 負 x 分 勝 負 分
以前の VS ai2 98.2 0.1 1.6 89.4 2.5 8.1 93.8 1.3 4.9
今回の VS ai2 98.4 0.1 1.5 89.3 2.3 8.4 93.8 1.2 4.9
以前の VS ai7s 33.4 3.9 62.6 3.9 28.8 67.3 18.7 16.4 65.0
今回の VS ai7s 33.1 3.9 62.9 3.8 29.2 67.0 18.4 16.6 65.0

ai8s の修正

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

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

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

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

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

    return ai_by_score(mb, eval_func, debug=debug) 
修正箇所
from ai import ai_by_score

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

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

    return ai_by_score(mb, eval_func, debug=debug) 

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

動作の確認

先程と 同様 に、ai2、ai7s と 対戦 し、以前の対戦結果と 比較 することで、ai8s が 正しく動作するか どうかを 確認 します。まず、ai2 と対戦します。

ai_match(ai=[ai8s, ai2])

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

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

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

次に、ai7s と対戦します。

ai_match(ai=[ai8s, ai7s])

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

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

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

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

関数名 o 勝 o 負 o 分 x 勝 x 負 x 分 勝 負 分
以前の VS ai2 98.2 0.1 1.6 89.4 2.5 8.1 93.8 1.3 4.9
今回の VS ai2 98.2 0.1 1.7 89.1 2.5 8.5 93.7 1.3 5.1
以前の VS ai7s 33.4 3.9 62.6 3.9 28.8 67.3 18.7 16.4 65.0
今回の VS ai7s 33.2 4.3 62.5 3.9 29.6 66.5 18.5 17.0 64.5

NamedTuple

Python には、namedtuple に、要素 の データ型のヒント を表す 型アノテーション の 機能を追加 した NamedTuple という クラス が typing モジュール に 定義 されています。NamedTuple と namedtuple の 違い は 以下 の 3 点 で、それ以外 の 機能は同じ です。

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

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

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

NamedTuple のインポート

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

from typing import NamedTuple

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

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

class クラスの名前(NamedTuple):
    要素の名前: 型アノテーション # この行を要素の数だけ記述する
  • 1 行目:通常の クラスの定義 と 同様の記述 を行うが、クラスの名前 の 直後 に (NamedTuple)5を 記述 する点が 異なる。namedtuple と 異なり、カスタムデータ型 の 名前 は typename ではなく、クラスの名前 で 設定 する
  • 2 行目以降:先頭の要素 から 順番 に、1 行ずつ、要素の名前: 型アノテーション の 形式 で 要素の名前 と、その要素の データ型のヒント を 記述 する

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

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

また、1 行目 の 直後 に、通常のクラス と 同様 に docstring を 記述 することが できます。docstring を 記述 する際に、属性の名前 は Attributes セクション に 記述 します。

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

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

NamedTuple のインスタンスの作成

namedtuple の場合と 同様 に、以後は NamedTuple の カスタムデータ型 から作成した データ のことを、単に NameTuple と 表記 します。

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

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

実行結果

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

動作の確認

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

先程と 同様 に、ai2、ai7s と 対戦 し、以前の対戦結果と 比較 することで、ai8s が 正しく動作するか どうかを 確認 します。まず、ai2 と対戦します。

ai_match(ai=[ai8s, ai2])

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

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

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

次に、ai7s と対戦します。

ai_match(ai=[ai8s, ai7s])

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

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

ratio     win    lose    draw
o       33.2%    4.2%   62.7%
x        3.9%   29.4%   66.7%
total   18.6%   16.8%   64.7%

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

関数名 o 勝 o 負 o 分 x 勝 x 負 x 分 勝 負 分
以前の VS ai2 98.2 0.1 1.6 89.4 2.5 8.1 93.8 1.3 4.9
今回の VS ai2 98.5 0.1 1.5 89.3 2.4 8.3 93.9 1.2 4.9
以前の VS ai7s 33.4 3.9 62.6 3.9 28.8 67.3 18.7 16.4 65.0
今回の VS ai7s 33.2 4.2 62.7 3.9 29.4 66.7 18.6 16.8 64.7

デフォルト値の設定方法

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

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

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

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

実行結果

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

NamedTuple の利点

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

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

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

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

NamedTuple の限界

NamedTuple による 型アノテーション は、関数の型アノテーション と同様に、ヒントにすぎない ため、強制力 は ありません。従って、下記のプログラムのように、型アノテーション と 異なるデータ型 を 要素 に 代入 しても エラーは発生しない 点に 注意 して下さい。

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

実行結果

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

変数の型アノテーション

以前の記事では、関数 の 仮引数 と 返り値 の 型アノテーション を紹介しましたが、Python では、ローカル変数、グローバル変数、属性 などに対しても、仮引数 と 同様 に、名前: 型アノテーション という方法で 型アノテーション を 記述 することが できます。

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

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

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

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

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

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

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

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

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

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

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

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

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

x

実行結果

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

NameError: name 'x' is not defined

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

x: int

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

print(x)

実行結果

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

NameError: name 'x' is not defined

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

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

class Point(NamedTuple):   
    x
    y

実行結果

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

NameError: name 'x' is not defined

tuple の型アノテーション

以前の記事で、list の 型アノテーション の 記述方法 について 説明 しましたが、tuple の 型アノテーション について 説明していなかった ので、この機会に 説明 します。

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

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

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

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

dataclass

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

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

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

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

dataclass を使わない実装方法

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

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

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

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

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

実行結果

1 2

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

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

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

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

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

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

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

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

<__main__.Point object at 0x000001D513E31A90>

比較に関する欠点

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

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

実行結果

False

これは、自分で定義 した クラス から 作成 された インスタンス を == 演算子 で 比較 した場合は、インスタンス の オブジェクトの id が 比較される からです。以前の記事で説明したように、異なるオブジェクト には 異なる id が 割り当てられる ので False になります。

dataclass の使い方と利点

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

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

dataclass のインポート

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

from dataclasses import dataclass

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

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

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

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

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

@dataclass
class Point:
    x: int
    y: int

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

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

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

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

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

実行結果

Point(x=1, y=2)

== による比較の利点

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

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

実行結果

True
False

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

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

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

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

実行結果

Point(x=1, y=2)

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

  • 同じ属性の値 を持つ、p1 と p2 を 作成 する
  • p1 と p2 の z という 属性 に 別の値 を 代入 する
p1 = Point(1, 2)
p2 = Point(1, 2)
p1.z = 3
p2.z = 5
print(p1 == p2)

実行結果

True

dataclass の使い道

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

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

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

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

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

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

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

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

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

自分 で __init__ メソッドを 定義してはいけない。

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

dataclass の仕組み

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

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

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

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

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

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

@dataclass
class Point:
    x: int
    y: int

    def __init__(self):
        pass

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

p = Point(1, 2)

実行結果

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

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

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

p = Point()
print(p.x)

実行結果

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

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

__post_init__ メソッド

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

@dataclass
class Point:
    x: int
    y: int

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

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

print(Point(1, 2))

実行結果

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

dataclass と NamedTuple の違い

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

ミュータブルである

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

print({Point(1, 2)})

実行結果

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

TypeError: unhashable type: 'Point'

tuple ではない

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

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

実行結果

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

TypeError: cannot unpack non-iterable Point object

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

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

print({Point(1, 2)})

実行結果

{Point(x=1, y=2)}

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

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

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

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

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

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

    return ai_by_score(mb, eval_func, debug=debug)  

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

from ai import ai6, ai7

ai_match(ai=[ai6s, ai6])

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

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

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

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

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

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

    return ai_by_score(mb, eval_func, debug=debug)  

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

ai_match(ai=[ai7s, ai7])

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

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

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

今回の記事のまとめ

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

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

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

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

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

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

次回の記事

  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?