LoginSignup
0
0

Pythonで〇×ゲームのAIを一から作成する その52 set の性質と利用方法

Last updated at Posted at 2024-02-08

目次と前回の記事

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

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

これまでに作成した 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 で表現 しましたが、list の要素 には、同じデータ複数存在できる ので、例えば下記のプログラムのように、ゲーム開始時 の局面に対して、enum_markpats を呼び出して 表示 すると、同じ マークのパターンが 8 回も表示 されてしまいます。

from marubatsu import Marubatsu
from pprint import pprint

mb = Marubatsu()
pprint(mb.enum_markpats())

実行結果

[defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'.': 3})]

プログラムが正しく動作する かどうかという 観点 では、list でも 全く問題 はないのですが、enum_markpats知りたい のは、特定マークのパターン存在するか どうかなので、同じ マークのパターンを表す データ が、上記のように 8 つも存在 するのは 無駄 と言えるでしょう。この問題は、set という データ型 を利用することで 解決 できます。

英単語 としての set集合 を表し、Pythonset文字通り集合扱う ための データ型 です。setlist のように 頻繁に使われる わけでは ありません が、適切な場面利用 することで、集合の性質 を持つ データを扱う プログラムが 記述しやすく なったり、処理効率を上げる ことができるので、今回の記事では、使い方を説明することにします。

集合とは何か

set の説明を行う前に、数学の用語 としての 集合説明 を行います。

数学用語集合 とは、ものの集まり を表す 用語 で、集合 の中に 含まれるもの のことを、(げん) または 要素(element)と呼びます。本記事 では 要素表記 します。

集合 は、現代数学 における 最も基本 となる 概念 の一つです。このように説明すると、難しそうだ と思う方が多いかもしれませんが、本記事では、それほど 難しい 集合の性質は 扱わない ので心配する必要はありません。

参考までに、wikipedia の集合の項目のリンクを下記に示します。

有限集合と無限集合

集合には、整数の集合 のように、無限個要素を持つ ものがあります。無限個 の要素を持つ集合を 無限集合、数に限りがある 有限個 の要素を持つ集合を 有限集合 と呼びます。Pythonset無限集合扱う ことは できない と思います1ので、本記事では、有限集合のみ扱い、以降は 集合 と記述した場合は、有限集合 のことを 表す ことにします。

集合の性質

集合 は、主に下記のような 性質 を持ちます。なお、厳密集合の性質 について知りたい方は、数学学ぶ必要 があります。

  • 集合の中同じ要素重複 して 存在しない
  • 集合の中要素順番存在しない
  • 要素存在しない 場合も、集合みなされる。そのような集合を、空集合 と呼ぶ
  • あるもの が集合の 要素含まれるか どうかを表す、帰属関係考える ことができる
  • ある集合すべての要素 が、別の集合要素含まれるか どうかを表す、包含関係考える ことができる
  • 集合どうし に対する、さまざまな演算 を行うことができる。例えば、集合 A集合 B に対して、集合和 という 演算 を行うことで、A と B要素を持つ 集合が 作成 される

enum_markpats が計算する、マークのパターン列挙 した データ は、以下のように、集合の性質満たす ので、そのデータを 集合とみなす ことができます。

  • 同じ マークのパターン重複する必要がない
  • 順番関係がない
  • 特定マークのパターン含まれるか という、帰属関係 を考えることができる

ピンとこない人がいるのではないかと思いますので、上記集合の性質 について、wikipedia の リンク先で紹介されている トランプ具体例 として説明します。

集合の例

上記のリンク先で例として挙げられている トランプ を使った 集合の例 を紹介します。集合 は、一般的に { } の中要素,区切って表記 するので、そのように表記します。

トランプマークの集合 は { ♥, ♠, ♦, ♣ } の 4 つ要素 を持つ集合です。

トランプ数字の集合 は { A, 2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, K } の 13 個要素 を持つ集合です。なお、この集合の中には AJQK という 文字含まれて いますが、多くの場合 では、それらは 1111213 という 数字みなされる ので、本記事では トランプ数字の集合 を { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 } とします。

同じ要素が重複しない

トランプ では、それぞれのマーク を持つカードは 13 枚ずつ ありますが、トランプマーク の集合では、トランプの カード から マークだけ取り出した、{ ♥, ♥, ♥, (♥ が 13 個並ぶ), ♠, ♠, ・・・以下略 } という、マークを並べたもの集合考える ことは ありません。その 理由 は、集合 が、その中特定のものが存在するか どうかを 表す ことが 目的作られて おり、ものが 何個存在する かという 情報不要 だからです。従って、重複 するものは 1 つまとめられトランプマーク の集合は、{ ♥, ♠, ♦, ♣ } となります。

集合の要素に順番はない

集合要素の中特定の要素 が存在するかどうかを表す際に、要素順番 の情報は 必要 では ありません。例えば、トランプマーク を表す集合は、♥、♠、♦、♣ の 4 つの要素持つ という 性質のみ求められる ので、マークの順番意味はありません

集合表記 する際には、何らかの順番 で要素を 記述する必要 がありますが、その 順番意味はありません。そのため、{ ♥, ♠, ♦, ♣ } と { ♦, ♣, ♥, ♠ } のように、表記の順番異なっていても、この 2 つは 同じ集合みなされます

トランプ数字 の集合のように、集合に含まれる 要素そのもの大きさなど順番が存在する 場合は、一般的 に { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 } のように、分かりやすさ重視 して 順番 に並べて 表記 しますが、この場合でも、集合の要素の 順番意味はありません。従って、トランプ数字 の集合は、{ 2, 6, 13, 1, 11, 10, 12, 8, 9, 7, 4, 3, 5 } のように、任意の順番 で要素を 表記 しても かまいません

複数のもの扱う際 に、ものの 順番が重要 な場合は、集合利用 することは できない 点に 注意 して下さい。Python の場合は、set ではなく、list を利用 する 必要 があります。

例えば、トランプマーク数字種類 を扱いたい場合は、集合利用 することが できます が、トランプカードシャッフルした後並べられた 52 枚のカードの 数字の内容 を上から 順番扱いたい 場合は、「重複した数字扱う必要 がある」、「順番必要 である」という理由から、集合 として 扱う ことは できません

空集合の例

例えば、トランプ で「20 以上数字」の 集合 は、そのようなトランプのカードが 存在しない ので 空集合 になります。なお、数学 では 空集合 には { } などの 複数の表記 がありますが、Python{} を記述すると、空の dict表す 点に 注意が必要 です。

帰属関係

帰属関係 とは、あるもの が、集合含まれている ことを表す 関係 です。

帰属関係あるか どうかを 調べる ことは、無限集合 の場合はそれほど 簡単な話ではありません が、有限集合 の場合は、すべて集合の要素調べる ことで、要素の数が多い場合は時間がかかるかもしれませんが、必ず判定 することが できます

例えば、トランプマーク の集合に、♠ が 含まれ、■(四角の記号) が 含まれない ことは、4 つ の要素を 調べる ことで 簡単に判定 できます。

包含関係と相当関係

包含関係 は、〇×ゲームAI を作成 する際に 利用しない ので、難しいと思った方 は簡単に 目を通しておくだけで良い と思います。後で 必要になった時読み返して 下さい。

包含関係 とは、ある集合すべての要素 が、別の集合要素含まれる帰属関係 がある)という 関係 を表します。例えば、トランプ赤いマーク の集合である { ♥, ♦ } の すべて要素 は、トランプマーク の集合の { ♥, ♠, ♦, ♣ } の 要素含まれる ので、包含関係 があります。包含関係 には 方向 があり、集合 Aすべての要素集合 B含まれる ことを、「A は B に包含される」または「B は A を包含する」のように 表記 します。

集合 A集合 B包含されるか どうかの 判定 は、Aそれぞれの要素 が、B帰属するか どうかを 判定 し、すべてA の要素B に帰属 する場合に「A は B に包含される」 と 判定 することが できます。従って、有限集合 の場合は、2 つ集合 の間に 包含関係あるか どうかを、必ず判定 することが できます

包含関係難しそう思える かもしれませんが、 で示すとわかりやすいでしょう。包含関係 は、下図の 黄色の集合 が、緑の集合の中完全に含まれる という関係です。

2 つの集合要素完全に等しい ことを「2 つの集合が 相当関係 である」、または 「2 つの集合が 等しい」と呼びます。A が B に包含される ことを、数学 では「$A ⊂ B$」と 表記 します。また、A と B相当である(等しい)ことを「$A = B$」、A と B が 相当でない(等しいくない)ことを「$A ≠ B$」と表記します。

A と B が相当であるか($A = B$)の 判定 は、包含関係 によって、「$A ⊂ B$ かつ $B ⊂ A$ である」場合に 相当 であると 判定 できます。証明は長くなるので省略しますが、それほど難しくはないので、興味がある方は調べてみて下さい。従って、有限集合 であれば、必ず相当関係があるか どうかを 判定 することが できます

A が B に包含される」($A ⊂ B$)場合のことを「A は B の 部分集合」であると表記します。また、「A が B に包含される」が「A と B は相当でない」場合($A ⊂ B$ かつ $A ≠ B$)のことを「A は B の 真部分集合」と表記します。

A が B に包含される」($A ⊂ B$)が 満たされない 場合に、「B が A に包含される」($A ⊃ B$)が 満たされるとは限らない 点に 注意 して下さい。例えば、トランプ偶数 の集合 { 2, 4, 6, 8, 10, 12 } を A3 の倍数 の集合 { 3, 6, 9, Q } を B とした場合に、集合 A は下図の 黄色 の楕円、集合 B緑色 の楕円で 表されます。図からわかるように、A は B包含されません が、B も A包含されません

集合どうしの演算の例

集合どうし演算 も、〇×ゲームの AI では 利用しない ので、ここでは 集合和 という演算 のみを紹介 します。他の演算については、この後で紹介します。

トランプ で、偶数の数字 の集合 { 2, 4, 6, 8, 10, 12 } と 3 の倍数の数字 の集合 { 3, 6, 9, Q } の 集合和 は、それらの 両方要素 を持つ { 2, 3, 4, 6, 8, 9, 10, 12 } になります。

集合和 は、下図の 黄色の集合 と、緑の集合両方の要素含む 集合になります。

set の使い方

Python の set は、上記のような 有限集合性質 を持つデータを表現する データ型 です。

上記で説明した 集合の性質対応 しながら、set の使い方説明 を行います。

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

set の作成

set は以下の いずれかの方法作成 することができます。

set のリテラル

setリテラルlist似ています が、外側囲う記号[] ではなく、{} である点が 異なります。なお、{}囲んで記述 する点は dict のリテラルと 同じ ですが、キー:キーの値 ではなくset の要素 となるデータ だけを記述 する点が 異なります

下記は、トランプマーク を要素とする setprint で表示 するプログラムです。print で表示 した場合は、set のリテラル同じ内容表示 されます。

print({"", "", "", ""})

実行結果

{'♠', '♥', '♦', '♣'}

set は、集合同様 に、同じ要素重複しない という 性質を持つ ので、下記のプログラムのように、setリテラル に、同じデータ複数記述 した場合は、同じデータ1 つにまとめられた set作成 されます。

print({"", "", "", "", "", "", "", "", "", "", "", ""})

実行結果

{'♠', '♥', '♦', '♣'}

空の set のリテラル

空集合 を表す、要素存在しない set のことを、空の set と呼びます。空の setリテラルだけ は、{} ではなくset() のように 記述 する 必要がある 点に 注意 して下さい。その 理由 は、{} のように 記述 すると、空の dictみなされる からです。空の setprint で表示 すると、下記のプログラムの 1 行目 のように set() が表示 されます。

print(set())  # 空の set のみ、必ずこのように記述する必要がある
print({})     # こちらは空の dict を表す

実行結果

set()
{}

list からの作成

上記で利用した、set は、データ型 を表す 組み込み型のクラス です。下記のプログラムのように、set引数list を記述 することで、記述した list と同じ要素 を持つ setインスタンス作成 することができます。2 行目 のように、同じ要素複数持つ list記述 した場合は、先程と同様 に、同じデータ1 つまとめられた set作成 されます。

print(set(["", "", "", ""]))
print(set(["", "", "", "", "", "", "", "", "", "", "", ""]))

実行結果

{'♦', '♥', '♣', '♠'}
{'♦', '♥', '♣', '♠'}

なお、下記のプログラムのように、組み込み型のクラスである list実引数set を記述 することで、setlist に変換 することも できます。そのことは、実行結果外側の記号 が、list を表す [] であることから 確認 できます。

print(list({"", "", "", ""}))

実行結果

['♠', '♥', '♦', '♣']

set 内包表記

list 内包表記同様set 内包表記 で、下記のプログラムのように set を作成 することが できますlist 内包表記 との 違い は、外側の記号{} である点 だけ です。

print({ i for i in range(10) })

実行結果

{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

set が扱えるデータ

list は、任意オブジェクト要素 にできましたが、set以前の記事で説明した ハッシュ可能 なオブジェクト のみ要素できます

list のような、ハッシュ可能 でない オブジェクトを set の要素 にしようとすると、下記のプログラムのように エラーが発生 します。なお、set がハッシュ可能なオブジェクトしか扱えない理由については後で説明します。

print({1, []})

実行結果

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

TypeError: unhashable type: 'list'

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

  • TypeError
    データ型(type)に関するエラー
  • unhashable type: 'list'
    list はハッシュ可能(hashable)なデータ型(type)ではない(un)

帰属関係の判定

あるデータ が、set要素含まれるか どうかという 帰属判定 は、下記のプログラムのように、list と同様 の方法で、in 演算子 を使って 判定できます

marks = {"", "", "", ""}
print("" in marks)  # ♠ は marks に含まれる
print("" in marks)  # ■(四角)は marks に含まれない

実行結果

True
False

set含まれない かどうかの 判定 も、list と同様not in 演算子 を利用できます。

marks = {"", "", "", ""}
print("" not in marks)
print("" not in marks)

実行結果

False
True

包含関係と相当関係(等しい)の判定

下記の表の 比較演算子 によって、set どうし包含関係相当関係判定 することができます。下記の「True になる条件」の は、演算子の左右AB という 2 つset を記述して 比較 した場合の 説明 です。また、参考までに 数学の用語 を表に示します。

演算子 True になる条件 数学の記法と用語
== A と B が等しい(同じ要素を持つ) $A = B$
A と B は相当(等しい)
!= A と B が等しくない(異なる要素がある) $A ≠ B$
> B のすべての要素を A が含む
ただし、A と B が等しい場合を除く
$A ⊃ B$ かつ $A ≠ B$
B は A の真部分集合
>= B のすべての要素を A が含む
A と B が等しくても良い
$A ⊃ B$
B は A の部分集合
< A のすべての要素を B が含む
ただし、A と B が等しい場合を除く
$A ⊂ B$ かつ $A ≠ B$
A は B の真部分集合
<= A のすべての要素を B が含む
A と B が等しくても良い
$A ⊂ B$
A は B の部分集合

set には、上記の演算子同じ処理 を行う メソッド が用意されています。また、上記 では 行えないAB共通の要素を持たない ことを 判定 する isdisjoint というメソッドがあります。興味がある方は、このリンク先を参照して下さい。

比較演算子の利用例

下記のプログラムは、上記の演算子利用例 です。AB は、setリテラル を記述する際に、異なる順番4 つのマーク記述 していますが、先ほど説明したように、集合の要素順番はない ので、AB同一setみなされますその他 については、一つ一つを説明すると長くなりすぎるので説明は 省略 します。上記の表 と、下記のプログラムの 実行結果 を各自で 見比べて下さい

A = {"", "", "", ""}
B = {"", "", "", ""}
C = {"", ""}            # 赤いマークのみの集合
D = {"", "", ""}       # トランプのマークと関係ない ■ が混じっている

print("A == B", A == B)
print("A == C", A == C)
print("A == D", A == D)
print()

print("A != B", A != B)
print("A != C", A != C)
print("A != D", A != D)
print()

print("A > B", A > B)
print("A > C", A > C)
print("A > D", A > D)
print()

print("A >= B", A >= B)
print("A >= C", A >= C)
print("A >= D", A >= D)
print()

print("B < A", B < A)
print("C < A", C < A)
print("D < A", D < A)
print()

print("B <= A", B <= A)
print("C <= A", C <= A)
print("D <= A", D <= A)

実行結果

A == B True
A == C False
A == D False

A != B False
A != C True
A != D True

A > B False
A > C True
A > D False

A >= B True
A >= C True
A >= D False

B < A False
C < A True
D < A False

B <= A True
C <= A True
D <= A False

list に対して == 演算子比較 した場合は、下記のプログラムのように、要素の順番含めて同じ でなければ Trueみなされませんその点に注意 して下さい。

A = ["", "", "", ""]
B = ["", "", "", ""]

print(A == B)

実行結果

False

set どうしの演算

下記 の表の 演算子 によって、set どうし演算 を行い、新しい set作成 することができます。下記の説明では、AB という 2 つの集合演算子で演算 した場合の説明です。

演算子 関係
| A と B の 両方の要素 を持つ、和集合 を計算する
& A と B の 共通する要素 を持つ、積集合 を計算する
- A の要素からB の要素削除 した、差集合 を計算する
B にのみ 存在する要素は 差集合 には 含まれない
^ ABいずれかのみ に含まれる、対象差 を計算する

下記のプログラムは、上記の演算子利用例 です。実行結果からわかるように、一部の演算 では、print で表示 される 要素の順番変化 しますが、先ほど説明したように set要素の順番意味はない ので、表示の順番変わっても問題はありません

A = {2, 4, 6, 8, 10, 12}  # トランプの偶数の数字
B = {3, 6, 9, 12}         # トランプの 3 の倍数の数字

print("A | B", A | B)
print("A & B", A & B)
print("A - B", A - B)
print("A ^ B", A ^ B)

実行結果

A | B {2, 3, 4, 6, 8, 9, 10, 12}
A & B {12, 6}
A - B {8, 2, 10, 4}
A ^ B {2, 3, 4, 8, 9, 10}

上記のうち、和集合 を計算する 演算子 は、数値和を計算 する際に利用する + ではない 点に 注意が必要 です。実際に set に対して + 演算子計算 を行おうとすると、下記のプログラムのように、エラーが発生 します。

print({1, 2} + {3})

実行結果

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

TypeError: unsupported operand type(s) for +: 'set' and 'set'

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

  • TypeError
    データ型(type)に関するエラー
  • unsupported operand type(s) for +: 'set' and 'set'
    + 演算子は set と(and)set 型(types)の値(operand2)の演算をサポートしていない(unsupported)

set和集合 の計算に +使われない理由 の一つは、set に対する 和集合 の計算では、重複する要素1 つまとめられる からです。それに対して、list の結合 を行う + 演算子 では、下記のプログラムのように、重複する要素まとめられる ことは ありません

print([1, 2, 3] + [1, 2])

実行結果

[1, 2, 3, 1, 2]

他の理由 としては、和集合の計算 が、まだ説明していない、| 演算子を使う、ビット演算論理和の計算似ている という理由があると思います。

set に対するさまざまな操作

set に対して行うことができる、さまざまな操作 について説明します。

add メソッドによる要素の追加

set に対して 要素を追加 する場合は、下記のプログラムのように、add メソッド を利用します。list に要素を追加する append メソッド とは 名前が異なる 点に 注意 して下さい。

a = { 1, 2 }
a.add(3)          # append ではない点に注意
print(a)

実行結果

{1, 2, 3}

また、以前の記事のノートで紹介した、list要素の追加 を行う際に利用できた += 演算子 は、set に対して行うと、下記のプログラムのように エラーが発生 する点にも 注意が必要 です。同様の処理 は、次に説明する |= 演算子 で行うことができます。

a += {4, 5}

実行結果

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[32], line 1
----> 1 a += {4, 5}

TypeError: unsupported operand type(s) for +=: 'set' and 'set'

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

  • TypeError
    データ型(type)に関するエラー
  • unsupported operand type(s) for +: 'set' and 'set'
    += 演算子は set と(and)set 型(types)の値(operand2)の演算をサポートしていない(unsupported)

|= 演算子による要素の追加

|= 演算子を使って、set別の set要素すべて追加 することができます。下記のプログラムの 3 のように、重複する要素 がある場合は、一つまとめられます

a = {1, 2, 3}
a |= {3, 4, 5}
print(a)

実行結果

{1, 2, 3, 4, 5}

|= 演算子に関する注意点

|= 演算子は、見た目上 は、前後の set に対して | 演算子 によって 和集合計算 し、|= の前記述された変数代入 するという処理を行うように 見えるかもしれません

例えば、下記のプログラムの 1 行目 と、2 行目同じ処理 を行うように 見えるかもしれません が、実際 には 異なる処理行う 点に 注意が必要 です。

a |= {3, 4, 5}
a = a | {3, 4, 5}

上記の 1 行目 では、a代入 された set+=右の set要素が追加 されます。一方 2 行目 では、a | {3, 4, 5}計算結果 を表す 新しい set作成 され、その 新しい seta参照 する オブジェクトを更新 する点が異なります。

  • |= 演算子では、|=前に記述 された 変数参照 するオブジェクトは 変化しない
  • = 演算子では、=前に記述 された 変数参照 するオブジェクトは 変化する

そのことは、下記のプログラムのように、変数参照 する オブジェクトの id表示 することで 確認 できます。この違い理解していないバグが発生 する 可能性 があります。具体例については、そのような状況が実際に起きる場合に紹介する予定です。

a = {1, 2, 3}
print(id(a))     
a |= {3, 4, 5}     # こちらは、a に要素を追加する処理
print(id(a))       # 2 行前と同じ id が表示される

a = {1, 2, 3}
print(id(a))
a = a | {3, 4, 5}  # こちらは、新しい set を作成し、a に代入する処理
print(id(a))       # 2 行前と異なる id が表示される

実行結果

1661492119360
1661492119360
1661492117568
1661492120704

なお、この性質 は、list に対する += 演算子 や、 で説明する 演算子 にも 共通 します。

set の内容を更新する他の演算子

先程紹介した、&-^ に対応する &=-=^= 演算子があります。いずれも、演算子の前 に記述された 変数 に代入されたの set の内容 を、対応する演算子 による 演算の結果更新 する処理を行い、|= と同様 に、変数参照 する オブジェクト変化しません

下記のプログラムは、それらの演算子の 使用例 です。これらの 演算子 による 演算によってa の値更新される ので、次の演算行う前a元の値代入しなおし ています。なお、b変化しない ので 代入し直す必要ありません

a = {2, 4, 6, 8, 10, 12}
b = {3, 6, 9, 12}

a &= b
print(a)

a = {2, 4, 6, 8, 10, 12}
a -= b
print(a)

a = {2, 4, 6, 8, 10, 12}
a ^= b
print(a)

実行結果

{12, 6}
{2, 4, 8, 10}
{2, 3, 4, 8, 9, 10}

要素の削除

下記の表は、set要素を削除 する方法です。pop 以外メソッド値を返しません

方法 意味
remove メソッド 実引数 に記述したものと 同じ要素削除 する
記述 した 要素が存在しない 場合は、エラーが発生 する
discard メソッド 実引数 に記述したものと 同じ要素削除 する
記述 した 要素が存在しない 場合は、何も行わない
pop メソッド いずれか 1 つ の要素を 削除 し、削除した値返す
なお、削除する要素指定できない
clear メソッド すべての要素削除 し、空の set にする
-= 演算子 演算子の右記述 した set の要素すべて削除 する

下記のプログラムは、それぞれの 利用例 です。-= 演算子先程説明した ので 省略 します。なお、pop メソッド は、個人的にはあまり 使い道がない のではないかと思います。

a = {1, 2, 3, 4, 5}

a.remove(5)   # 5 を削除する
print(a)

a.discard(4)  # 4 を削除する
print(a)

a.discard(6)  # 存在しない 6 を discard で指定した場合は何も起こらない
print(a)

print(a.pop()) # いずれか 1 つの要素を削除し、その値を返す
print(a)       # 上の行で表示された値の要素が削除されている

a.clear()      # すべての要素を削除する
print(a)       # 空の set を表す set() が表示される

実行結果

{1, 2, 3, 4}
{1, 2, 3}
{1, 2, 3}
1
{2, 3}
set()

下記のプログラムは、remove メソッド で、存在しない要素削除 しようとする例で、実行結果からわかるように、エラーが発生 します。

a = {1, 2, 3}
a.remove(5)

実行結果

---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[19], line 2
      1 a = {1, 2, 3}
----> 2 a.remove(5)

KeyError: 5

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

  • KeyError
    キー(key)に関するエラー
  • 5
    5 というキーは存在しない

要素の数の計算

set要素の数 は、list と同様 に、組み込み関数 len を使って下記のプログラムのように 計算できます

print(len({1, 2, 3}))
print(len(set()))      # 空の set の場合の要素の数は 0

実行結果

3
0

for 文による要素の列挙

set反復可能オブジェクト なので、for 文反復可能オブジェクト記述 することで、下記のプログラムのように、list と同様要素順番に取り出す ことができます。

なお、set要素 は、set挿入した順番取り出され ますが、&= 演算子などによって set要素を更新 した場合などで、順番が変更される 場合があります。ただし、set要素の順番意味はない ので、表示される順番重要ではありません

print("list")
for i in [1, 2, 3]:
    print(i)
print("set")
for i in (1, 2, 3):
    print(i)

実行結果

list
1
2
3
set
1
2
3

インデックスは利用できない

set は、要素の順番意味はない ので、list のように、インデックス を使って 特定の順番の要素取りだしたり値を変更 することは できません。下記のプログラムのように setインデックスを記述 して 要素を参照 しようとすると、エラーが発生 します。

a = {1, 2, 3}
print(a[1])

実行結果

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[22], line 2
      1 a = {1, 2, 3}
----> 2 print(a[1])

TypeError: 'set' object is not subscriptable

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

  • TypeError
    データ型(Type)に関するエラー
  • set object is not subscriptable
    set は、[] を使ってその中のデータを参照する(subscript)ことはできない(is not able)

set と list の使い分け

ここまでの説明で setlist似ている部分が多い と思った方が多いのではないかと思います。実際setlist は、多くの点似ていますが下記 の点が 大きく異なる ので、状況に応じて 適切に 使い分ける必要 があります。

  • set は、その 要素の中 に、同じデータ複数持つ ことが できない
  • set の 要素の順番意味を持たない
  • set の 要素 を、インデックス を指定して 参照 することは できない。できるのは、特定の値set要素の中存在するか どうかを 判定する(帰属判定)こと だけ である
  • set は、要素追加と削除 を行うことはできるが、要素の値直接変更できない
  • set は、ハッシュ可能 なオブジェクト しか扱えない

上記の性質のうち、最後の性質以外 は、数学の 集合の性質 です。従って、set は、複数のデータ集合として扱いたい 場合に 利用 します。具体的には、主に、下記1 ~ 3性質を持つ データを 扱う際利用 します。

  1. 下記の いずれか(または複数)の 目的複数のデータ扱いたい
    1. 複数のデータ の中に、特定のデータ存在するか帰属関係) を 判定したい
    2. 複数のデータどうし が、包含関係相当関係 にあるかを 判定したい
    3. 複数のデータどうし の、和集合など集合としての演算行いたい
  2. 複数のデータ順番個数扱う必要がない
  3. 複数のデータ内容 を後から 変更しない

enum_markpats計算 する、マークのパターン一覧 は、上記の 1-1、2、3性質を満たす データなので、set を使って 表現 することが できます

上記の 3 の性質意味分かりづらい と思いますので補足します。

例えば、enum_markpats によって、マークのパターン一覧 として「自 1 敵 0 空 2」と「自 0 敵 0 空 3」いう 2 つマークパターン計算 された場合のことを考えてみて下さい。局面変わらなければ、その局面の マークのパターン一覧変わりません。従って、局面変わらない限りマークのパターン一覧いずれか を例えば「自 1 敵 1 空 1」のように 変更 しては いけない ことがわかります。これが、「内容後から変更しない」ということの意味です。

従って、逆に 下記いずれか性質を 持つ データ を扱いたい場合は、set ではなく、listtupledict などを利用する 必要 があります。ただし、3 つ目の性質必要な場合tuple利用できません

  1. 複数のデータ を、順番を付けて 管理したい
  2. 同じデータ複数個 管理したい
  3. 管理するデータ内容後から変更 したい

set がハッシュ可能なオブジェクトしか扱えない理由

set性質 の中で、ハッシュ可能 なオブジェクト しか扱えない という 性質 が、集合性質 とは 関係ない ように 見える かもしれませんので、この性質 がある 理由 を説明します。

処理を高速に行うため

set に対して 行うことができる、下記のような set に対して 行うことができる さまざまな演算 では、特定のデータset の要素等しいか どうかを 判定する必要 があります。また、ハッシュ可能なオブジェクト は、ハッシュ値利用 することで、等しいかどうか を、高速に判定 できます。そのため、set要素ハッシュ可能なオブジェクト限定する ことで、set に関する 多くの処理高速に行う ことが できるようになります

  • in 演算による 帰属関係判定
  • 比較演算子 による 包含関係相当関係判定
  • 和集合などset どうしさまざまな演算

そのことを 確認 するために、setlist の両方で 共通 して 行う ことが できるin 演算子 による 帰属関係判定処理速度 を、setlist比較 することにします。

なお、下記の比較 は、ざっくりとした比較 なので、厳密 な set と list の 処理速度の比較 には なっていません が、処理速度圧倒的な差 から、要素が多い場合 は、set のほうが list より高速処理を行う ことが できる という 事実 は間違いなく 確認できます

下記のプログラムは、0 ~ 100000 未満 の、10 万個整数要素 として持つ listset内包表現 を使って 作成 し、ls という名前の変数に代入しています。

l = [ i for i in range(100000) ] # 10 万個の要素を持つ list
s = { i for i in range(100000) } # 10 万個の要素を持つ set

次に、下記のプログラムで、0 ~ 100000 未満10 万個整数l の要素に 含まれるか どうかを in 演算子判定 し、その数を数える という処理を行います。10 万個すべて整数l に含まれる ので 実行結果 には 10万表示 されます。筆者のパソコン で、下記のプログラムを実行すると 約 54.6 秒 かかりました。つまり、一回in 演算子 の判定で、平均 するとその 10 万分の 1 の、約 0.546 ミリ秒処理時間 がかかります。

count = 0
for i in range(100000):
    if i in s:
        count += 1
print(count)

実行結果

100000

次に、下記のプログラムで set代入 された s に対して 同じ処理 を行います。筆者のパソコン で、下記のプログラムを実行すると、一瞬処理が終 了し、JupyterLabセル には 0.0 秒表示 されました。

count = 0
for i in range(100000):
    if i in s:
        count += 1
print(count)

実行結果

100000

あまりに速く 計算が行われてしまったため、JupyterLabセルの表示 では 処理時間 をうまく 計測できなかった ので、in 演算子処理 を行う 回数 を下記のプログラムのように、100 倍1000 万回 行うことにします。なお、s の要素には、10 万まで整数 しか 存在しない ので、下記のプログラムの 3 行目 のように、余りを計算 する % 演算子 を使って i10 万で割った余りs の要素に 存在するか どうかを 計算する ことで、常に s要素が存在 する in 演算子計算を行う ようにしています。

筆者のパソコン で、下記のプログラムを実行すると 約 1.8 秒 の時間がかかりました。つまり、一回in 演算子 の判定で、平均 するとその 100 万分の 1 の、約 0.0018 ミリ秒処理時間 がかかります。

count = 0
for i in range(10000000):
    if i % 100000 in s:
        count += 1
print(count)

実行結果

10000000

筆者のパソコン では、1 回あたりin 演算子処理速度 は、list の場合は 約 0.546 ミリ秒set の場合は、約 0.0018 秒 かかったので、set のほうが 0.546 ÷ 0.0018 = 約 300 倍速度処理を行う ことができることが分かりました。

このように、ハッシュ可能なオブジェクト要素の値限定 することで、同じ in 演算子 による 処理 を行った場合でも、listset では 処理時間大きな差生じます

要素の数と処理速度の差の関係

なお、詳しい説明は省略しますが、in 演算子 による 処理速度 は、list の場合 は、list要素の数ほぼ比例 しますが、set の場合ほとんど変わりません。例えば、要素の数 を下記のプログラムのように、1/101 万減らして 先程と 同様の処理 を行ってみます。

l = [ i for i in range(10000) ] # 1 万個の要素を持つ list
s = { i for i in range(10000) } # 1 万個の要素を持つ set

下記のプログラム(3 行目i % 10000修正 しています)を実行すると、筆者のパソコン では、先程約 1/10約 5.6 秒処理時間 がかかりました。

count = 0
for i in range(100000):
    if i % 10000 in l:
        count += 1
print(count)

一方、下記のプログラムを実行すると、筆者のパソコン では 約 1.8 秒 かかり、処理時間変わらない 事が分かります。なお、処理時間計測 は、パソコンの状態 によって 全く同じ計算 を行った場合でも 若干の変化 があります。この例では 処理時間変わりません でしたが、理論的 には set の要素の数少なくなる処理時間若干短く なります。

より厳密処理時間計測方法 については、今後の記事紹介 します。

count = 0
for i in range(10000000):
    if i % 10000 in s:
        count += 1
print(count)

このように、setlist処理速度 は、要素の数大きい程、顕著な 差が生じます が、要素の数10 程度 であれば、処理速度ほとんどありません。実際に、下記のプログラムで、要素の数10listset1000 万回in 演算子処理時間計測 した所、list約 2.1 秒set約 1.4 秒 となり、処理時間あまり変わりません でした。

先程言及したように、要素の数1 万より 大きく 少ない 10 の場合は、set処理時間 が、先程約 1.8 秒より若干短く なっています。

なお、それぞれ処理時間JupyterLabセルの表示計測 するためには、下記の 3 つ のプログラムを 別々のセル実行する必要 がある点に 注意 して下さい。

l = [ i for i in range(10) ] # 10 個の要素を持つ list
s = { i for i in range(10) } # 10 個の要素を持つ set
count = 0
for i in range(10000000):
    if i % 10 in l:
        count += 1
print(count)
count = 0
for i in range(10000000):
    if i % 10 in s:
        count += 1
print(count)

このことから、in 演算子 を使って 帰属判定 を行う際に、setlistどちら を使っても 良い場合 では、要素の数数十超える ことが 判明 した時点で、set積極的に利用 したほうが 良い ことが分かります。

残念ながら、〇×ゲームマークのパターン の場合は、最大 でも 要素の数8 しかない ので、処理速度 の面では setlist大きな差はない でしょう。そのことは、set を利用 するプログラムで ai8s修正後検証 することにします。

set の性質を満たすため

set の性質 に、要素の値変更できない というものがあります。これは、先程のノートマークのパターン集合 を例に挙げて 説明 したように、数学集合を扱う際 に、一度 集合の要素入れた ものの 内容後から変更する という処理を 行わない からです。

しかし、set要素list のような ハッシュ可能でないミュータブル なデータを 代入できてしまう と、set対する処理 によって 要素の値変更できなくてもlist の要素変更 することで、間接的set の要素変更できてしまう ことになります。

下記のプログラムは、実際に 実行するとエラー になりますが、set の要素 に、list を代入できる 場合は、間接的set要素の値変更できてしまいます

a = [3, 4, 5] 
b = {1, 2, a} # a は list なので実際にはこの行を実行するとエラーになる
a[0] = 5      # 仮に上記でエラーが発生しない場合は、この処理で、b の要素の値を変更できてしまう

以前の記事で説明したように、ハッシュ可能なオブジェクト であれば、上記 のようなことは基本的には 起きない3ので この問題発生しません

set と list の比較

setlist性質が似ている ので、今回の記事で紹介した set の性質 に関して list と比較 したものを下記の表にまとめます。

list set
リテラルの外側の記号 [] で要素を囲む {} で要素を囲む
リテラルの要素の区切り , で区切る list と同じ
空のデータ [] set()
内包表記の外側の記号 [] で要素を囲む {} で要素を囲む
扱えるのデータの種類 任意のオブジェクト ハッシュ可能なオブジェクト
要素の順番 順番がある 順番はない
要素の変更 追加、削除、変更 追加、削除のみ
インデックスで参照できるか 可能 不可能
要素に含まれるかの判定 innot in 演算子 list と同じ
== 演算子での True の判定 順番も含めて等しい 同じ要素を持つ
演算子による演算 + * による結合 | & - ^ による集合演算
要素の追加のメソッド append add
要素の数を数える組み込み関数 len list と同じ
要素の列挙 for 文により可能 list と同じ

set を使った ai8s の実装

set使い方 を説明したので、次に、set を使って処理を行うように ai8s修正 します。

enum_markpats の修正

ai8sset利用するため には、enum_markpats返すデータ を、下記のプログラムのように list から set に修正 する必要があります。setlist使い方かなり似ている ので、下記の部分だけ修正 するだけで済みます。

  • 2 行目markpats代入 する値を、空の list から 空の set修正 する
  • 7、9、12、15 行目append メソッドを、add メソッドに 修正 する
 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)
 7          markpats.add(count)
 8          count = self.count_marks(coord=[i, 0], dx=0, dy=1)
 9          markpats.add(count)
10      # 左上から右下方向の判定
11      count = self.count_marks(coord=[0, 0], dx=1, dy=1)
12      markpats.add(count)
13      # 右上から左下方向の判定
14      count = self.count_marks(coord=[2, 0], dx=-1, dy=1)
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)
        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
修正箇所
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実装確認 する際に記述したプログラムです。このプログラムを実行すると、下記のような エラーが発生 します。

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[33], line 7
      5 for i in range(self.BOARD_SIZE):
      6     count = self.count_marks(coord=[0, i], dx=1, dy=0)
----> 7     markpats.add(count)
      8     count = self.count_marks(coord=[i, 0], dx=0, dy=1)
      9     markpats.add(count)

TypeError: unhashable type: 'collections.defaultdict'

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

  • SyntaxError
    データ型(type)に関するエラー
  • unhashable type: 'collections.defaultdict'
    collections モジュールの defaultdict は、ハッシュ可能(hashable)なデータ型(type)ではない(un)

この エラー は、ハッシュ可能でないdefaultdictset の要素 として 追加 しようとしたことが原因です。従って、この エラー修正 するためには、ハッシュ可能なオブジェクト返す ように、count_marks修正 する 必要 があります。

今回の記事のまとめ

長くなりましたので、今回の記事はここまでにし、修正は次回の記事で行うことにします。なお、上記の enum_markpats には バグがある ので、marubatsu.py には 反映させません

今回の記事では、集合 を扱う データ型 である set に関する 説明 を行いました。また、set を使って ai8s修正 する際に エラーが発生する ことを説明しました。

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

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

今回の記事では marubatsu.py と ai.py は変更していません。

次回の記事

  1. 違っていれば指摘していただければ嬉しいです

  2. オペランド(operand)とは、演算子演算の対象 とする のことです 2

  3. 正確には、例外 として、自分で定義 した クラスインスタンス の場合は 間接的 にその 属性の値変更できる ので、その点に注意 する 必要 があります

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