LoginSignup
0
0

Pythonで〇×ゲームのAIを一から作成する その10 正しい初期化されたゲーム盤の作成方法

Last updated at Posted at 2023-09-19

目次と前回の記事

正しい初期化されたゲーム盤の作成方法

前回の記事で、下記の欠陥のあるゲーム盤を初期化するプログラムで行われる処理を説明するために必要な前提知識の説明が終わりました。

board = [[" " * 3] * 3]

実は、上記のプログラムで行われる処理を理解するためには、まだいくつか説明しなければならないことがあります。それはこのプログラムで記述されている * 演算子 についての説明です。また、* 演算子を説明するためには、list に対する + 演算子 の説明をする必要があります。

+ 演算子による list の結合

2 つの list に対して + 演算子で計算を行うと、2 つの list の要素を 結合 する処理が行われます。list の結合とは、list の要素の末尾に、別の list の要素を加えた 新しい list を作成する という処理の事です。

下記のプログラムで具体例を示します。1 行目の処理は比較的簡単に理解できると思いますが、2 行目は少しわかりづらいかもしれません。

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

実行結果

[1, 2, 3, 4, 5]
[1, 2, 3, [1, 2], [3, 4]]

2 行目の処理を理解するためには、list の結合処理の手順を以下のように考えると良いでしょう。

  1. 結合する 2 つの list の外側の [] をそれぞれ削除して 要素を並べる
  2. 上記の 1 つ目の list の要素を並べたものの 末尾に , を加え、その後に 2 つめの list の要素を並べたものを並べる
  3. 全体を [] で囲う

この手順で実際に上記のプログラムの 2 つの処理を行ってみます。なお、背景が灰色で表示されていない部分は、結合処理をわかりやすく説明するための 架空のデータ で、結合処理の過程で実際にそのようなデータが作られているわけではない点に注意して下さい。

  • 1 行目の処理
    1.  [1, 2, 3] → 1, 2, 3
       [4, 5] → 4, 5
    2.  1, 2, 3, 4, 5
    3.  [1, 2, 3, 4, 5]
  • 2 行目の処理
    1.  [1, 2, 3] → 1, 2, 3
       [[1, 2], [3, 4]] → [1, 2], [3, 4]
    2.  1, 2, 3, [1, 2], [3, 4]
    3.  [1, 2, 3, [1, 2], [3, 4]]

+ 演算子による list の結合処理は、新しい list を作成する 処理です。式に記述した list は いずれも変更されない 点に注意して下さい。

よくある 勘違い として、下記のようなプログラムを実行すると、2 行目の a + [2] の部分の処理で、a に代入された list が変更されるというものがあります。

実行結果からわかるように、実際には a の値は [1] のまま 変化せずb には a + [2]、すなわち [1] + [2] という結合処理によって 新しく作成された [1, 2] が代入されます。

a = [1]
b = a + [2]
print(a)
print(b)

実行結果

[1]
[1, 2]

+ 演算子と異なり、append は list の末尾に 要素を追加 して、list の要素を変更する 処理です。list の要素を変更する処理は、append 以外では、+= という演算子で行うことができます。具体的には以下のように記述することで、list が代入された変数に、反復可能オブジェクトから取り出したデータを順番に要素として追加するという処理が行われます。なお、このような処理の事を、list の拡張 と呼ぶことがあります。

listが代入された変数 += 反復可能オフジェクト

下記のプログラムは、+= 演算子を使ったプログラムの具体例です。

a = [1, 2]
a += [3, 4, 5]
print(a)

実行結果

[1, 2, 3, 4, 5]

また、+= と同様の処理を extend を使って以下のように記述することもできます。

a = [1, 2]
a.extend([3, 4, 5])
print(a)

実行結果

[1, 2, 3, 4, 5]

list と list 以外のデータを + 演算子で計算するとエラーが発生します。下記のプログラムでは、list と 整数型のデータを + 演算子で計算したためエラーが発生しています。

[1] + 2

実行結果

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[5], line 1
----> 1 [1] + 2

TypeError: can only concatenate list (not "int") to list

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

  • TypeError
    データ型(Type)に関するエラー
  • can only concatenate list (not "int") to list
    list には(to)、整数型("int")ではなく、list しか(only)結合(concatenate)できない(can)

* 演算子による list の結合

list に対して * 演算子で計算を行うと、その list と 同一のデータ* 演算子の右に記述された 整数回 だけ + 演算子で 結合 するという処理が行われます。ここでいう同一のデータとは、同じオブジェクトが管理する データの事です。

この、同一のデータ に対して結合処理が行われるという点を 理解せず* 演算子を使用すると、思わぬバグの原因 になってしまう点に注意が必要です。この後で詳しく説明しますが、board = [[" " * 3] * 3] が間違った初期化されたゲーム盤のデータになるのは、そのことが原因 になっています。

* 演算子でよくある誤解が、下記の 1 行目と 2 行目が同じ処理を行う式だと思ってしまうことです。実行結果を見ると、ab の表示が同じになるので、同じ処理を表す式のように 見える かもしれませんが、この 2 つの式は 異なる結合処理 を表しています。

a = [1] * 3
b = [1] + [1] + [1]
print(a)
print(b)

実行結果

[1, 1, 1]
[1, 1, 1]

2 つの処理の違いは、[1] * 3 が、必ず同一の オブジェクトが管理するデータ を 3 回結合するのに対して、[1] + [1] + [1]異なる オブジェクトが管理するデータを結合するという点にあります。

上記で、[1] + [1] + [1]異なる オブジェクトが管理するデータを結合すると説明しましたが、実際にはプログラムの 処理の効率化 などの 目的 で、同一のオブジェクトが管理するデータを結合する場合があります。ただし、そのような現象が起きるのは、同一のオブジェクトが管理するデータを結合しても、その後の プログラムの処理に影響が出ない 場合に限られます。

なお、どのような場合にプログラムの処理に影響が出ないかについては、次回の記事で説明します。

参考までに、[1] + [1] + [1] のような処理で、同一のオブジェクトが管理するデータが結合される場合と、必ず異なるオブジェクトが管理するデータが結合される場合の例を紹介します。

[1] + [1] + [1] や、[" "] + [" "] + [" "] のように、list の要素に「同じ内容の 数値型」や、「同じ内容の 文字列型」のデータが代入された複数の list を + 演算子で結合すると、同一のオブジェクトが管理するデータが結合される場合があります。

下記の実行結果は、筆者のコンピューターで下記のプログラムを実行した場合の結果です。コンピューターの OS や Python のバージョンによって異なる id が表示される場合があります。

実行結果から、結合された list が代入された a の 3 つの要素が、全て同じ オブジェクトを 参照 することがわかります。b に関しても同様です。

a = [1] + [1] + [1]
b = [" "] + [" "] + [" "]
print(id(a[0]), id(a[1]), id(a[2]))
print(id(b[0]), id(b[1]), id(b[2]))

実行結果

140731432669992 140731432669992 140731432669992
140731432712392 140731432712392 140731432712392

一方、[[" "]] + [[" "]] + [[" "]] のように、「同じ値の要素を持つ list」を要素として持つ 複数の list+ 演算子で結合すると、必ず異なった オブジェクトが管理するデータが結合されます。下記のプログラムの実行結果からそのことを確認できます。

a = [[" "]] + [[" "]] + [[" "]]
print(id(a[0]), id(a[1]), id(a[2]))

実行結果

2115175036160 2115175118784 2115174884416

[1] * 3 と同じ処理を行うプログラムは、以下のように記述できます。2 行目で、list の 3 つの要素に、1 が代入された a を記述することで、1 を管理する 同一の オブジェクトを参照する 3 つの要素を持つ list が b に代入されます。

a = 1
b = [a, a, a]
print(b)

実行結果

[1, 1, 1]

* 演算子で結合 された list と、同じ内容の list を + 演算子 で結合して作られた list に大きな違いはないと思うかもしれませんが、この違いが 重要 になる場合とそうでない場合があります。

欠陥のあるゲーム盤を初期化するプログラムの処理

欠陥のあるゲーム盤を初期化するプログラムである、下記のプログラムの 1 行目で行われる処理を順を追って説明します。その後で、2 行目の処理を実行した結果、複数のセルを表す要素が同時に変更されてしまう理由について説明します。

board = [[" "] * 3] * 3
board[1][0] = ""
print(board)

実行結果

[['〇', ' ', ' '], ['〇', ' ', ' '], ['〇', ' ', ' ']]

[" "] * 3 で行われる処理

[[" "] * 3] * 3 は、list の中に list が 入れ子 になっている点が わかりづらい ので、このプログラムを list が 入れ子にならないように見える プログラムに変更することにします。

[[" "] * 3] * 3 の中に記述されている、[" "] * 3 は、ゲーム盤の 列(row) を表すデータです。そこで、[" "] * 3row という変数に代入することで、上記のプログラムを以下のように list が 入れ子にならないように見える プログラムに変更することができます。

下記のプログラムの 2 行目は、list が入れ子になっていないように見えるかもしれませんが、実際には row に代入された値は list なので、list の入れ子が 解消されているわけではありません

row = [" "] * 3
board = [row] * 3

先ほど説明したように、上記のプログラムの 1 行目の row = [" "] * 3 は、下記のプログラムの 1、2 行目のように変更することができます。

a = " "
row = [a, a, a]
board = [row] * 3

上記のプログラムの 1 行目の処理は、単純な代入文なので説明を省略し、2 行目で行われる処理について説明します。下図は、上記のプログラムの 2 行目の処理を図示したものです。

row に代入されている list の 3 つの要素である row[0]row[1]row[2] は、同一のオブジェクトを参照することで、同一の " " というデータを共有しています。この 3 つの要素はゲーム盤の列の セル を表すデータなので、それぞれに 別々のデータを代入 できる必要があります。

そのことを下記のプログラムで確認します。下記のプログラムは、上記のプログラムの 2 行目の後で、row[0] = "〇" という処理を追加していますが、実行結果から値が変更されるのは row[0] だけrow[1]row[2] の値は 変化しない ことがわかります。実際のプログラムは示しませんが、row[1] = "〇" のような処理を行った場合でも同様です。

上記の事から row初期化された列のデータ として 利用できる ことが確認できました。

a = " "
row = [a, a, a]
print(row)
row[0] = ""
print(row)

実行結果

[' ', ' ', ' ']
['〇', ' ', ' ']

これは、row[0] = "〇" という代入処理で、下図のような処理が行われるからです。なお、 a はこのプログラムではもう利用しないので、図から削除しました。

下図はプログラム A の 3 行目で行われる処理です。一見すると上図と下図で異なる種類の処理が行われているように見えるかもしれませんが、「上図のインデックスが 0 の要素と、下図の a」、「上図のインデックスが 1、2 の要素と、下図の b」が対応していると考えて見比べてみると、この 2 つの処理が 同じ種類の処理 を行っていることがわかります。

[" "] * 3 の処理の要点をまとめると以下のようになります。

  • row = [" "] * 3 で行われる処理は プログラム A の 1、2 行目で行われる処理と 同じ種類の処理 である
  • 従って、プログラム A の ab のように、ゲーム盤のセル を表す row の要素別々の値を代入 することができる
  • 従って、初期化されたゲーム盤の を表す 1 次元配列 のデータとして、[" "] * 3 を利用することができる

また、上記の要点から、[[" "] * 3] * 3 の中で、[" "] * 3 の部分は 間違っていない ことがわかります。

[[" "] * 3] * 3 で行われる処理

[[" "] * 3] * 3 と同じ処理を行う下記のプログラムの 1、2 行目で行われる処理の説明をしましたので、次は下記の 3 行目で行われる処理を説明します。

a = " "
row = [a, a, a]
board = [row] * 3

先程と同様に、上記の 3 行目のプログラムは、下記の 3 行目のプログラムのように変更することができます。2 行目のプログラムと 3 行目のプログラムを見比べると、変数名が異なる 以外は 全く同じ処理 が記述されており、「2 行目の row と 3 行目の board」、「2 行目の a と 3 行目の row」が対応していることがわかります。

a = " "
row = [a, a, a]
board = [row, row, row]

下図は、上記のプログラムの 3 行目の処理を図示したものです。

下図は、上記のプログラムの 2 行目の処理を図示したものです。上図と下図を見比べてみて、プログラムの 2 行目と 3 行目の対応通りに、「上図の board が 下図の row」に、「上図の row が 下図の a」に対応していることを確認して下さい。

board に代入されている list の 3 つの要素である board[0]board[1]board[2] は、同一のオブジェクトを参照することで、同一の [" ", " ", " "] というデータを共有しています。この 3 つの要素は ゲーム盤の列 を表すデータです。

下記のプログラムは、ゲーム盤の列 を表す 3 つの要素に格納されているオブジェクトの id を表示しています。実行結果で同じ id が表示されることから、それぞれの要素が実際に 同一の オブジェクトを参照していることがわかります。

a = " "
row = [a, a, a]
board = [row, row, row]
print(id(board[0]))   # ゲーム盤の 0 列を表す list を管理するオブジェクトの id 
print(id(board[1]))   # ゲーム盤の 1 列を表す list を管理するオブジェクトの id 
print(id(board[2]))   # ゲーム盤の 2 列を表す list を管理するオブジェクトの id 

実行結果

2115175037952
2115175037952
2115175037952

board[1][0] = "〇" で行われる処理

board = [[" "] * 3] * 3 を実行した際に、board にどのようなデータが代入されているかがわかりましたので、いよいよ問題となっている board[1][0] = "〇" を実行した時にどのような処理が行われるかについて説明します。

a = " "
row = [a, a, a]
board = [row, row, row]
board[1][0] = ""
print(board)

実行結果

[['〇', ' ', ' '], ['〇', ' ', ' '], ['〇', ' ', ' ']]

下図は、上記のプログラムの 4 行目の board[1][0] = "〇" で行われる処理を図示したものです。

一方、下図はプログラム B の 3 行目の a[0] = 2 で行われている処理です。board[1][0] は上図の ID が 117 のオブジェクトが管理するデータの 0 番の要素で、a[0] は下図の ID が 150 のオブジェクトが管理するデータの 0 番の要素です。この 2 つを対応させて 上図と下図を見比べると、この 2 つの処理が 同じ種類の処理 を行っていることがわかります。

このように、board = [[" "] * 3] * 3 と、プログラム B では、同じ種類の処理が行われています。そのため、プログラム B の a[0] に対応する board[1][0] に値を代入すると、プログラム B の ab に対応する board[0]board[1]board[2] の値が同時に変更されるように見えます。実際には、プログラム B と同様に、board[0]board[1]board[2]同じデータ共有 しているためそのような現象がおきるのです。

board[1][0]ゲーム盤のセル を表すため、他のセルを表す board の要素の要素に 別々のデータを代入 できる必要があります。しかし、board[0][0]board[1][0]board[2][0]同じデータを共有 してるため、別々のデータを代入することはできません。そのため、board をゲーム盤を表すデータとして使用することは できない のです。

[[" "] * 3] * 3 の処理の要点をまとめると以下のようになります。

  • board = [[" "] * 3] * 3 で行われる処理は プログラム B の 1、2 行目で行われる処理と 同様の処理 である
  • 従って、プログラム B のように、ゲーム盤のセル を表す board の要素の要素 に値を代入すると、同時に ゲーム盤の列 を表す board の要素 が変更されてしまう
  • 従って、初期化された ゲーム盤 を表す 2 次元配列 のデータとして、[[" "] * 3] * 3 を利用することはできない

下記は、先ほど示した [" "] * 3 の要点です。下記では、上記と 異なっている 部分を 太字 で表記しました。下記の太字の部分が上下でどのように異なっているかに注目して上下の要点を見比べてみて下さい。

[" "] * 3 の処理の要点をまとめると以下のようになります。

  • row = [" "] * 3 で行われる処理は プログラム A の 1、2 行目で行われる処理と同様の処理である
  • 従って、プログラム A の ab のように、ゲーム盤のセルを表す row の要素別々の値を代入することができる
  • 従って、初期化された ゲーム盤 の列 を表す 1 次元配列のデータとして、[" "] * 3 を利用することが できる

上記の 2 つの要点の重要な相違点は以下の 2 点です。

1 つは 3 つめの箇条書きから、 [" "] * 3ゲーム盤の列 を表す 1 次元配列 のデータであるのに対し、[[" "] * 3] * 3ゲーム盤そのもの を表す 2 次元配列 のデータであるということです。この 2 つのデータは、どちらも同じような list に見えるかもしれませんが、データの意味 と配列の 次元の数 が異なります。

もう 1 つは、2 つめの箇条書きの「ゲーム盤のセルを表す」の直後の「board の要素の要素」と「row の要素」の部分です。「board の 要素の要素」という表現がわかりにくいと思いますが、board は list の要素の中に list が代入された 2 次元配列 を表すデータです。board1 つ目 のインデックスで「ゲーム盤の列」のデータが代入された 要素 を参照し、2 つ目のインデックスで「ゲーム盤のセル」が代入された要素を参照しているので、「要素の要素」という表現になっています。

この違いを表にすると以下のようになります

[" "] * 3 [[" "] * 3] * 3
データを代入する変数名 row board
データの意味 ゲーム盤の列 ゲーム盤
ゲーム盤のセルを表すデータ row の要素 board の要素の要素
セルの値の変更で行われる処理 変更するのは、row要素 なので、他のセルを表す row の要素の値は 変更されない 変更するのは board要素の要素 である。ゲーム盤の列を表す board要素 は、同じデータを共有 しており、セルの値を変更する際に board の要素は どれも変更されない

* 演算子を使って list を結合する場合は、以下の点に注意して下さい。

  • 結合する list の要素に、数値型や文字列型のデータが代入されている場合は、* 演算子でその list を結合しても問題は発生しない
  • 結合する list の要素に、list が代入されていた場合は、結合された list の要素は、同一の list を共有 してしまうため、思わぬバグの原因になってしまう可能性が高いので 避けたほうが良い

数値型、文字列型、list 以外のデータの場合については、次回の記事で説明します。

rowboard の違い

row = [a, a, a] では、ゲーム盤のセルを表す要素に異なる値を代入できるのに、board = [row, row, row] ではそれができないことの理由がまだピンとこない人がいるかもしれないので補足します。

そのような人は、おそらく row[0] = "〇" という処理と、board[1][0] = "〇" という処理が似ているように見えることが混乱の原因になっているのではないかと思います。確かにどちらも、ゲーム盤のセルを表す要素 に値を代入するという意味では同じ種類の処理を行っています。

しかし、row[0] = "〇" は、「row に代入された list の要素」に値を代入する、board[1][0] = "〇" は、「board に代入された list の要素の要素」に値を代入するという意味では 異なる種類の処理 が行われています。

変数に代入された list の要素」に値を代入するという意味では、row[0] = "〇" に対応する処理は、board[1] = ["〇", " ", " "] です。この処理によって、ゲーム盤の x 座標が 1 の列を表す要素に新しい list が代入され、board[1]board[0]board[2] のデータの共有が解除されます。そのため、board の要素 にゲーム盤の列を表す list を代入することで、board[0]board[1]board[2]別々の 列のデータを代入することができます。

この説明と同じような説明をどこかで見たような気がした人はいませんか?これは、前回までの記事で、プログラム A と B の処理の違いを説明する際に行った、「a = 2a[0] = 2見た目は似ている が、異なる処理 を行う」、「プログラム A の a = 2 に対応するプログラム B の処理は a = [4, 5, 6] である」という説明と同じです。

下記は、欠陥のあるゲーム盤を初期化するプログラムの後で、上記の board[1] = ["〇", " ", " "] を実行するプログラムです。実行結果からわかるように、ゲーム盤の列 を表す要素に 別々のデータを代入 することができます。

a = " "
row = [a, a, a]
board = [row, row, row]
board[1] = ["", " ", " "]
print(board)

実行結果

[[' ', ' ', ' '], ['〇', ' ', ' '], [' ', ' ', ' ']]

ゲーム盤に対して行う処理の内容が、「ゲーム盤の を変更する」という 処理だけ であれば、board = [[" "] * 3] * 3 を使って、初期化されたゲーム盤を作成しても問題はありません。しかし、実際には「ゲーム盤の セル を変更する」という処理が必要です。そのため、board = [[" "] * 3] * 3 をゲーム盤のデータとして利用することはできません。

正しく動作するプログラムへの修正方法

board = [[" "] * 3] * 3 の問題点は、この処理を以下のように変更した際の 3 行目で、board の要素に、同一のオブジェクト を参照する list を代入している点にあります。

a = " "
row = [a, a, a]
board = [row, row, row]

上記のプログラムの 3 行目で、board の要素に [" ", " ", " "] という、同じ内容の list を管理 する、異なるオブジェクト を参照する list を代入すればこの問題を解決することができます。

* 演算子を使って list を作成すると、必ず同一の list を参照するオブジェクトが要素に代入されるので、上記のプログラムを * 演算子を 使わない ように修正する必要があります。

そのような修正は、以下のプログラムのように、リスト内包表記 を使うことで行うことができます。下記のプログラムを実行すると、実行結果から (1, 0) を表す要素のみが "〇" になっており、正しい初期化されたゲーム盤のデータが作成されていることを確認できます。

board = [[" "] * 3 for x in range(3)]
board[1][0] = ""
print(board)

実行結果

[[' ', ' ', ' '], ['〇', ' ', ' '], [' ', ' ', ' ']]

上記のプログラムが正しく動作する理由は、リスト内包表現を使わないように、上記のプログラムを変更すると理解しやすいでしょう。下記のプログラムはそのように変更したプログラムです。

board = []
for x in range(3):
    board.append([" "] * 3)
board[1][0] = ""
print(board)

実行結果

[[' ', ' ', ' '], ['〇', ' ', ' '], [' ', ' ', ' ']]

プログラムの 3 行目の board.append([" "] * 3) は、3 回繰り返して実行されます。list の結合処理 は、新しい list を作成する処理 であることを思い出してください。従って、繰り返しのたびに [" "] * 3 の部分で、同じ内容の list を管理する 異なる オブジェクトが作成されて、board の要素に追加されます。そのため、board[1][0] = "〇" のように、ゲーム盤のセルを表す、board の要素の要素に値を代入した際に、他のセルを表す要素が同時に変更されるという問題点が解消されています。

下記のプログラムは、ゲーム盤の列を表す 3 つの要素に格納されているオブジェクトの id を表示しています。実行結果から、表示される id が異なることから、それぞれの要素が実際に異なるオブジェクトを参照していることがわかります。

board = [[" "] * 3 for x in range(3)]
print(id(board[0]))   # ゲーム盤の 0 列を表す list を管理するオブジェクトの id 
print(id(board[1]))   # ゲーム盤の 1 列を表す list を管理するオブジェクトの id 
print(id(board[2]))   # ゲーム盤の 2 列を表す list を管理するオブジェクトの id 

実行結果

2115175038912
2115174964672
2115174967360

次回の記事の内容について

これで、board = [[" "] * 3] * 3 が欠陥のある処理を行っている理由と、正しい初期化されたゲーム盤を記述する方法を示すことができました。

ここまでの記事では、数値型文字列型list の の 3 つのデータ型に関する代入処理しか説明しませんでした。そのため、他のデータ型の代入処理でどのような処理が行われるかについて、疑問に思っている方も多いのではないかと思います。

そこで、次回の記事では今回までの記事で説明できなかった、代入処理に関する補足説明を行い、〇×ゲームの実装の再開は、その次の記事から行うことにします。

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

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

次回の記事

更新日時 更新内容
2023/09/29 for y in range(3)for x in range(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