LoginSignup
1
0

Pythonで〇×ゲームのAIを一から作成する その6 list と繰り返し処理

Last updated at Posted at 2023-09-19

目次と前回の記事

前回のおさらい

前回の記事では、〇×ゲームのゲーム盤を表す データ構造 を決め、そのデータ構造に従って 全てのマスが空のゲーム盤のデータ をプログラムで記述しました。

ただし、前回の記事で紹介したプログラムの記述方法は 効率が良くない ため、繰り返し処理を使って効率よく記述する方法を紹介することにしました。

そのための前提知識として、前回の記事では Python の繰り返し処理の基本的な説明を行いました。

「全てのマスが空のゲーム盤のデータ」という表現は冗長なので、以後は「初期化されたゲーム盤」と表記することにします。

繰り返し処理を使った Python の list の作成

今回の記事では、初期化されたゲーム盤を表す 2 次元配列の list を、繰り返し処理を使って記述する方法について説明します。

空の list

前回の記事では、下記のプログラムのように、list の要素に代入したいデータを 全て プログラムで 記述する ことで、初期化されたゲーム盤を表す 2 次元配列の list を記述していました。

board = [
    [" ", " ", " "], 
    [" ", " ", " "], 
    [" ", " ", " "]
]

繰り返し処理で上記と同じ list を作成する場合は、要素が一つもない list を作成し、その list に 後から要素を追加 するという方法でプログラムを記述します。

要素が 1 つもない list のことを 空の list と呼び、Python では空の list を [] のように記述します。

下記のプログラムは、a に空の list を代入する処理を行っています。

a = []
print(a)

実行結果

[]

上記のプログラムのように、変数に初めてデータを代入する処理の事を 変数の初期化 と呼びます。

list の要素の追加

Python の list は、下記のように記述することで、list の要素の 末尾新しい要素を追加 することができます1

listのデータ.append(新しい要素に追加するデータ)

list には、他にもいくつか要素を追加する方法があります。それらについては、必要になった時点で説明します。

配列の要素の末尾に新しい要素を追加する処理は、配列の種類やプログラム言語の違いによって、addpush など、異なる名前で記述する場合があるので、配列の種類やプログラム言語ごとに覚える 必要があります。

また、配列の種類によっては、最初に 配列の次元と要素数を 決めておく 必要があり、後から新しい要素を追加することができない ようなものもあります。Python の場合は、numpy というモジュールが提供する配列のデータがそのような性質を持ちます。

ゲーム盤の列を表す list の記述方法

過去の記事で説明したように、初期化されたゲーム盤を表す 2 次元配列の list は、下記のプログラムのように、ゲーム盤の列ごとに 全てのマスが空の列のデータ を表す list を一旦変数に代入し、それらを list の要素とするようなプログラムで書き直すことができます。

「全てのマスが空の列のデータ」も冗長なので、以後は「初期化された列」と表現します。

row0 = [" ", " ", " "]       # 0 列のデータ
row1 = [" ", " ", " "]       # 1 列のデータ
row2 = [" ", " ", " "]       # 2 列のデータ
board = [row0, row1, row2]   # ゲーム盤のデータ  

上記のプログラムから、初期化されたゲーム盤を表す 2 次元配列の list を作成するためには、先に 初期化された列を表す list を作成する必要があることがわかります。

初期化された列を表す list は、空のリストappend を使って 3 回 半角の空白文字が代入された 要素を追加 するという処理で作成することができます。下記は、初期化された 0 列を表す list を作成するプログラムです。実行結果を見て、目的の list が row0 に代入されていることを確認して下さい。

row0 = []
row0.append(" ")
row0.append(" ")
row0.append(" ")
print(row0)

実行結果

[' ', ' ', ' ']

上記のプログラムは、row0.append(" ") という、全く同じ処理 を 3 回繰り返す処理なので、for 文を使って下記のように記述することができます。

row0 = []
for y in range(3):
    row0.append(" ")
print(row0)

実行結果

[' ', ' ', ' ']

前回の記事で、for 文の中の、反復可能オブジェクトから取り出したデータを代入する変数の名前は、代入する データの内容 を表す 簡潔な名前 を使うのが一般的だということをノートで説明しました。

上記のプログラムでは、for 文の繰り返しのブロックの中で、0 列の 0、1、2 行 のデータを表す要素を追加しています。そのため、反復可能オブジェクトである range(3) から取り出したデータは、行の番号 を表します。ゲーム盤のセルの y 座標 が行の番号を表すので、変数の名前を y にしました。

なお、上記のプログラムでは、for 文の繰り返しのブロックの中で、この y という変数は どこにも使われていません。このような場合は、その変数が どこにも使われていない ということを 明確にするため に Python では _2 という名前の変数を使うという 慣習 があります。なお、これは慣習なので 必須ではありません

_ という名前の変数は、上記以外の場合でも、変数に値を代入する 必要はある が、その変数をプログラムの中で 使用しない 場合でも使われます。その使用例については具体例が出てきた時に説明します。

ただし、上記のプログラムでは、変数名を y としたほうが、for 文で 何を繰り返しているか明確になる ため、_ とするよりも わかりやすい と思いましたので、y という名前の変数にしています。

本記事でのプログラムの修正箇所の表記方法

プログラムを 少しずつ修正 しながらプログラムを作り上げていく場合、プログラムが長くなると、修正の際にどの部分が 変更されたかがわかりにくく なります。

そこで以降の記事では、以前のプログラムを 修正した プログラムを表記する際に、新しいプログラムだけでなく、修正箇所 がわかるようなプログラムの表記も行うことにします。

ただし、以下の場合は修正箇所を表記しません。

  • プログラムが短い場合など、修正箇所を 説明しなくても簡単に分かる ような場合
  • 変更箇所が多く、修正箇所を示すと逆に わかりづらくなる ような場合
  • 新しいプログラムを記述した場合

具体的には、以下のように修正箇所を表記します。なお、下記の表記方法は、プログラムの修正箇所を表記する方法として実際に良く使われる方法の一つです。

  • 新しく追加された 行の先頭に + を表示し、その行を 緑色の背景 で表示する
  • 前のプログラムから削除された 行の先頭に - を表示し、その行を 赤色の背景 で表示する
  • 前のプログラムから 行の一部が変更 された場合は、行の先頭に - を表示して 変更前の行 を表示し、その次の行の先頭に + を表示して 変更後の行 を表示する

追加、または削除されたことを表す行の先頭に表記される + または -プログラムの一部ではない 点に注意して下さい。

言葉の説明だけではわかりづらいと思いますので、以下に具体例を示します。下記では修正前のプログラムに対し、以下のような修正が行われています。

  • 1 行目の直後に b = a + 1追加する
  • 2 行目を print(a) から print(b)変更する
  • 3 行目の print(a * a)削除する

修正前のプログラム

a = 1          # この行は変更されない
print(a)       # この行は print(b) に変更される
print(a * a)   # この行は削除される

修正後のプログラム

a = 1          # この行は変更されない
b = a + 1      # この行が追加される
print(b)       # この行は print(a) から変更される

修正箇所は以下のように表示されます

a = 1            # この行は変更されない
+ b = a + 1      # この行が追加される
- print(a)       # この行は print(b) に変更される
+ print(b)       # この行は print(a) から変更される
- print(a * a)   # この行は削除される

修正箇所を常に表示すると、どうしても 記事が長く なってしまうので、普段は修正箇所を 折りたたんで表示されない状態 にします。

折りたたまれた修正箇所は、下記のように 左端に三角形のマーク と「修正箇所」のような文字が表示された行を クリック することで表示されるようになります。また、もう一度同じ行をクリックすると再び折りたたまれます。実際に下記の「修正箇所」と表記された部分をクリックして確認して下さい。

修正箇所
a = 1
+ b = a + 1
- print(a)
+ print(b)

2 次元配列を表す list の記述方法

初期化されたゲーム盤を表す 2 次元配列の list を作成するプログラムは、for 文を使って、下記のように記述し直すことができます。なお、下記のプログラムは修正箇所が多いので修正箇所は表記しません。

row0 = []              # 0 列のデータを作成する
for y in range(3):
    row0.append(" ")

row1 = []              # 1 列のデータを作成する
for y in range(3):
    row1.append(" ")

row2 = []              # 2 列のデータを作成する
for y in range(3):
    row2.append(" ")

table = [row0, row1, row2]
print(table)

実行結果

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

上記のプログラムでは、列ごとの処理を区別できるように 間に何もプログラムを記述しない行 を入れています。このような、何もプログラムを記述しない行 の事を 空行 と呼びます。空行は、プログラムを読みやすくする ために良く使われます。本記事でもプログラムが読みやすくなるように、必要に応じてプログラムに空行を記述します。

上記のプログラムは同じような処理を 3 回行っていますが、その 3 回の処理では row0row1row2 という 異なる名前 の変数が使われています。そのため、繰り返し処理でまとめることができないように思えるかもしれませんが、実際には 繰り返し処理でまとめる ことができます。その理由とまとめる方法について説明します。

上記のプログラムでは、row0 などに初期化された列を代入する処理を、繰り返し処理を使って行うように 元のプログラムから変更 していますが、table の処理は元のままで 変更していません。繰り返し処理を使って list を作成するためには、「空の list を代入」し、「要素を append で追加する」という処理を行う必要があるので、table の処理を下記のプログラムのように修正します。

table = []

row0 = []              # 0 列のデータを作成する
for y in range(3):
    row0.append(" ")
table.append(row0)     # table に 0 列のデータを追加する

row1 = []              # 1 列のデータを作成する
for y in range(3):
    row1.append(" ")
table.append(row1)     # table に 1 列のデータを追加する

row2 = []              # 2 列のデータを作成する
for y in range(3):
    row2.append(" ")
table.append(row2)     # table に 2 列のデータを追加する

print(table)
修正箇所
+ table = []

row0 = []              # 0 列のデータを作成する
for y in range(3):
    row0.append(" ")
+ table.append(row0)   # table に 0 列のデータを追加する

row1 = []              # 1 列のデータを作成する
for y in range(3):
    row1.append(" ")
+ table.append(row1)   # table に 1 列のデータを追加する

row2 = []              # 2 列のデータを作成する
for y in range(3):
    row2.append(" ")
+ table.append(row2)   # table に 2 列のデータを追加する

- table = [row0, row1, row2]
print(table)

実行結果

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

上記のように修正したプログラムでも、各列の処理で row0row1row2 という 異なる名前 の変数が使われているので繰り返し処理でまとめられないように思えるかもしれませんが、以下の手順でうまくまとめることができるようになります。

下記は、先ほどのプログラムから「0 列のデータを作成し、table の要素に追加」する処理を抜き出したものです。

row0 = []
for y in range(3):
    row0.append(" ")
table.append(row0)

このプログラムは 0 列のデータを作成する処理なので、row0 という名前の変数を使っていますが、この変数の名前を以下のプログラムのように row に変更 しても 全く同じ処理 が行われます。

row = []
for y in range(3):
    row.append(" ")
table.append(row)

同様に、残りのプログラムの row1row2row に変更すると以下のようなプログラムになります。

table = []

row = []               # 0 列のデータを作成する
for y in range(3):
    row.append(" ")
table.append(row)      # table に 0 列のデータを追加する

row = []               # 1 列のデータを作成する
for y in range(3):
    row.append(" ")
table.append(row)      # table に 1 列のデータを追加する

row = []               # 2 列のデータを作成する
for y in range(3):
    row.append(" ")
table.append(row)      # table に 2 列のデータを追加する

print(table)
修正箇所
table = []

- row0 = []               # 0 列のデータを作成する
+ row = []                # 0 列のデータを作成する
for y in range(3):
-    row0.append(" ")
+    row.append(" ")
-table.append(row0)       # table に 0 列のデータを追加する
+table.append(row)        # table に 0 列のデータを追加する

- row1 = []               # 1 列のデータを作成する
+ row = []                # 1 列のデータを作成する
for y in range(3):
-    row1.append(" ")
+    row.append(" ")
- table.append(row1)      # table に 1 列のデータを追加する
+ table.append(row)       # table に 1 列のデータを追加する

- row2 = []               # 2 列のデータを作成する
+ row = []                # 2 列のデータを作成する
for y in range(3):
-    row2.append(" ")
+    row.append(" ")
- table.append(row2)      # table に 2 列のデータを追加する
+ table.append(row)       # table に 2 列のデータを追加する

print(table)

実行結果

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

上記のプログラムでは、同じ名前row という変数を 使いまわして 0、1、2 列を表す list を作成し、table の要素に追加していますが、このような変数の使いまわしをしても大丈夫なのでしょうか?結論から言うと、この場合は同じ変数を 使いまわしても問題はありません。その理由を説明します。

このプログラムの 目的 は、初期化されたゲーム盤を表す 2 次元配列の list を作成し、table という変数に代入する ことです。row という変数は その処理を行う過程 で、初期化された列 を表す list を 一時的に保存しておくための入れ物 に過ぎません。

row = []
for y in range(3):
    row.append(" ")

上記の繰り返し処理によって row に代入された、初期化された列を表す list は、その次の table.append(row)table の要素として 追加された後 は、row の中に 記録しておく必要はなくなります。そのため、次の列の処理の最初で row = [] を実行して、row次の列のデータ を表す空の list を代入して中身を 上書き することで、row を別の用途で使いまわして 再利用する ことができます。

現実の例で例えると、メモ用紙に伝えたい内容を書きこんで、その内容を見せて伝える場合に似ています。メモの内容を見せて伝えた後は、そのメモ用紙に 書かれた内容は用済み になります。そのため、そのメモ用紙に 別の内容を書きこんで 見せるという、別の用途で使いまわして 再利用する ことができます。

従って、0、1、2 列の処理を行うプログラムで記述されている row は、いずれも 同じ変数 ですが、中に代入されている list はそれぞれ 異なる 列のデータを表しています。そのため、上記のプログラムは正しく動作します。

0、1、2 列の処理を行うプログラムの記述が、全く同じ になったので、上記のプログラムを for 文を使って以下のようにまとめることができます。わかりやすいように、プログラムにはコメントを入れておきました。

# table を空の list で初期化する
table = []
# 各列の繰り返しを行う
for x in range(3):
    # x 列のデータを表す row を、空の list で初期化する
    row = []
    # x 列の各行の繰り返しを行う
    for y in range(3):
        # x 列を表す list の要素に y 行のセルを表す半角の空白文字を追加する
        row.append(" ")
    # table に 初期化された x 列を表す row を追加する
    table.append(row)
print(table)

実行結果

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

このように、2 次元配列を表す list は、2 重に入れ子にした for 文 による繰り返し処理を使って記述することができます。

2 重の for 文による繰り返し処理は、プログラミングにおいて 頻繁に 出てくる 重要な処理 なので、その意味と使い方を必ず理解して下さい。

プログラミングでは一般的に、同じ名前の変数異なる用途使いまわすべきではない と言われています。その理由は、そのようなプログラムを記述すると、プログラムの中で使いまわした変数がどのような 意味を持つ かが 非常に分かりにくくなる からです。

ただし、同じ名前の変数に 同種の 異なるデータを代入して使いまわすということは実際に良く行われます。その具体例の一つが、繰り返し処理のブロックの中だけ で使われる変数です。

上記のプログラムでは、row という for x in range(3): の繰り返し処理の ブロックの中だけ で使われている変数は、一見すると使いまわされていないように 見える かもしれませんが、実際にはこの繰り返し処理の中で 異なる 「初期化された列を代入する」という目的で、同じ名前の変数 が 3 回 使いまわされています

他にも、同じ名前の変数を、プログラムの異なる場所で使いまわしても良い場合がありますが、それらについては具体例が出てきた時に説明します。

デバッグ出力を使った処理の流れの確認

2 重の for 文による繰り返し処理は、慣れるまでは どのような処理 が行われているかを 理解できない 人が多いと思います。プログラムの 処理の流れ確認 するために、print などを使って メッセージを出力(表示)するという方法があり、そのような表示の事を デバッグ出力 または デバッグ表示 と呼びます。

デバッグ(debug)とは、プログラムのエラーを表す バグ (bug)を 修正 して取り除く作業のことを表します。このように書くと、デバッグのことを 既に発見されている バグを修正する作業のように思う人がいるかもしれませんが、デバッグにはプログラムに 発見されていないバグ がないかどうかを 確認する 作業も含まれます。

プログラムの処理の流れを確認するという作業は、プログラムに バグがないかどうかを確認する ためにもよく行われます。そのため、プログラムの処理の流れを確認するための表示もデバッグ出力と呼びます。

他のデバッグ出力の例としては、計算結果のみ を表示して、おかしな計算が行われていないことを 確認する というものがあります。これまでのプログラムの最後で print(table) のように、変数の値を表示していたのは、プログラムで正しい計算が行われているかどうかを確認するための デバッグ出力 です。

デバッグ出力は、処理の流れを確認したい場所に print処理に関係する変数の値 などを表示するという方法で記述します。先ほどのプログラムの場合、tablerow の中身が、プログラムの処理によってどのように 変化していく かがわかれば、プログラムで行われている処理の流れを理解しやすくなります。

そこで、下記のプログラムでは、以下の処理が行われた直後に、print を記述して、xyrowtable などのデータを表示しています。なお、デバッグ出力の意味が分かりやすくなるように、表示内容を工夫をしました。

  • 列の繰り返し処理を開始した後で row[] を代入した直後
  • 行の繰り返し処理の中で row の要素に半角の空白文字を追加した直後
  • 列の繰り返し処理の最後で table の要素に row を追加した直後
# table を空の list で初期化する
table = []
# 各列の繰り返しを行う
for x in range(3):
    # x 列のデータを表す row を空の list で初期化する
    row = []
    # row の値をデバッグ出力する
    print("列の処理の開始 x =", x, "row =", row)
    # x 列の各行の繰り返しを行う
    for y in range(3):
        # x 列を表す list の要素に y 行のデータを表す空白文字を追加する
        row.append(" ")
        # row の値をデバッグ出力する
        print("x =", x, "y =", y, "row =", row)
    # table に x 列のデータを表す row を追加する
    table.append(row)
    # table の値をデバッグ出力する
    print("列の処理の終了 x =", x, "table =", table)
print(table)
修正箇所
# table を空の list で初期化する
table = []
# 各列の繰り返しを行う
for x in range(3):
    # x 列のデータを表す row を空の list で初期化する
    row = []
    # row の値をデバッグ出力する
+   print("列の処理の開始 x =", x, "row =", row)
    # x 列の各行の繰り返しを行う
    for y in range(3):
        # x 列を表す list の要素に y 行のデータを表す空白文字を追加する
        row.append(" ")
        # row の値をデバッグ出力する
+       print("x =", x, "y =", y, "row =", row)
    # table に x 列のデータを表す row を追加する
    table.append(row)
    # table の値をデバッグ出力する
+   print("列の処理の終了 x =", x, "table =", table)
print(table)

実行結果

列の処理の開始 x = 0 row = []
x = 0 y = 0 row = [' ']
x = 0 y = 1 row = [' ', ' ']
x = 0 y = 2 row = [' ', ' ', ' ']
列の処理の終了 x = 0 table = [[' ', ' ', ' ']]
列の処理の開始 x = 1 row = []
x = 1 y = 0 row = [' ']
x = 1 y = 1 row = [' ', ' ']
x = 1 y = 2 row = [' ', ' ', ' ']
列の処理の終了 x = 1 table = [[' ', ' ', ' '], [' ', ' ', ' ']]
列の処理の開始 x = 2 row = []
x = 2 y = 0 row = [' ']
x = 2 y = 1 row = [' ', ' ']
x = 2 y = 2 row = [' ', ' ', ' ']
列の処理の終了 x = 2 table = [[' ', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
[[' ', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]

実行結果のデバッグ出力を見て、rowtable がどのようにプログラムの処理によって変化していくかを確認して下さい。

デバッグ出力は print を記述するだけで表示できるので、簡単に記述 できて便利なのですが、デバッグ出力が 表示されすぎて 訳が分からなくなることがあるなどの欠点があります。

例えば、上記のプログラムの、ゲーム盤の 縦横のサイズ を表す 2 つの range(3)range(100) に変えて実行すると、100 × 100 = 10000 以上の 大量のデバッグ出力 が表示されてしまいます。そのような場合は、条件分岐などを使って 重要な場合だけに絞って デバッグ出力を行うように工夫する必要があります。

デバッグ出力は、バグが発生した際に、プログラムのどこに バグがあるかを探す 場合でも良く使われます。そのような場合は、プログラムの中で バグの原因になりそうな場所 でデバッグ出力を行うことで、エラーが起きている場所を 絞り込んでいく という方法でバグを探します。具体例については今後の記事で紹介する予定です。

プログラムの処理の流れを確認する他の方法としては、プログラムの開発環境の デバッガ機能 を使ってプログラムの処理を 1 行ずつ実行しながら追っていく という方法があります。こちらの方法はプログラムの流れを詳細に追っていくことができるので非常に 強力で便利 なのですが、うまく使いこなすためには ある程度の熟練 が必要です。VSCode の JupyterLab で Python のプログラムを実行する際でもこの機能を使うことができますが、その説明を行うと非常に長くなってしまうのでここでは割愛します。興味がある方は調べてみると良いでしょう。

リスト内包表記

Python では、for 文と append を使って list を記述するプログラムを、リスト内包表記 という方法で 簡潔に記述 することができます。リスト内包表記は、他のプログラム言語ではあまり見られない記法なので、初めて見る方は戸惑ったり、意味が分かりにくいと思うかもしれませんが、便利なので Python では 良く使われます

リスト内包表記は、下記のように記述します。

[  for 変数名 in 列挙可能オブジェクト ]

リスト内包表記で行われる処理は、それと 全く同じ処理 を for 文 と append を使って記述するプログラムを比較するのが理解しやすいでしょう。

下記の 2 つのプログラムは 全く同じ処理 を行うプログラムです。下のプログラムは 3 行のプログラムを記述する必要がありますが、上のリスト内包表記を使ったプログラムでは、同じ処理を 1 行で簡潔に記述 することができます。

上のプログラムと下のプログラがどのように対応するかを見比べてみて下さい。

a = [  for 変数名 in 列挙可能オブジェクト ]
a = []
for 変数名 in 列挙可能オブジェクト:
    a.append()

list 内包表記を使わないプログラムでは、最初に 空の list を変数に代入 する処理が 必要 になります。そのため、上のプログラムでも、list 内包表記で作られた list を、下のプログラムに合わせて a という変数に 代入 しています。

これだけではまだピンとこない人もいると思いますので、具体例をいくつか挙げます。

1 次元配列を表す list の例

下記の 2 つのプログラムは全く同じ処理を行います。2 つのプログラムを見比べて、どことどこが対応しているかについて確認して下さい。

a = [ i * i for i in range(10)]
print(a)
a = []
for i in range(10):
    a.append(i * i)
print(a)

実行結果はどちらも以下のようになります。

[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

2 次元配列を表す list の例

リスト内包表記を使えば、以下の表のような 2次元配列を表す list も、下記のプログラムのように簡潔に記述することができます。

0 1 2
0 0 1 2
1 3 4 5
2 6 7 8

なお、下記のプログラムは、この表の (x, y) のセル の内容が、x + y * 3 という式で計算できることを利用しています。

table = [ [ x + y * 3 for y in range(3) ] for x in range(3)]
print(table)

実行結果

[[0, 3, 6], [1, 4, 7], [2, 5, 8]]

なぜこのプログラムがうまく動作するかがわからない人もいると思いますので、上記のプログラムを、リスト内包表記を使わないプログラムに修正してみます。

上記のプログラムでは、リスト内包表記が 入れ子で記述 されている点がわかりにくさの原因になっています。そこで、プログラムをわかりやすくするために、外側のリスト内包表記 を、リスト内包表記を使わない プログラムに 修正 することにします。

下記はそのような修正を行ったプログラムです。この修正では、上記のプログラムの [ x + y * 3 for y in range(3) ] の部分を table = [ 式 for 変数名 in 列挙可能オブジェクト ] の「式」の部分だと考えると理解できると思います。

table = []
for x in range(3):
    table.append([ x + y * 3 for y in range(3) ])
print(table)

実行結果

[[0, 3, 6], [1, 4, 7], [2, 5, 8]]

上記の table.append()中の リスト内包表記を修正すると下記のプログラムのようになります。下記のプログラムは、リスト内包表記を説明する前に説明した、2 次元配列を表す list を表記する方法で記述されたプログラムです。従って、[ [ x + y * 3 for y in range(3) ] for x in range(3)] というリスト内包表現で、2 次元配列を表す list を正しく記述できることがわかります。

table = []
for x in range(3):
    row = []                  
    for y in range(3):          
        row.append(x + y * 3)
    table.append(row)
print(table)

実行結果

[[0, 3, 6], [1, 4, 7], [2, 5, 8]]

[ x + y * 3 for y in range(3) ] をリスト内包表記を使わないプログラムに修正する際には、その list を表すデータを 代入するための変数 を用意する必要があります。このデータは、表の列(row)を表すデータなので、その変数の名前を row としました。

〇×ゲームの初期化されたゲーム盤

〇×ゲームの初期化されたゲーム盤は、すべての要素半角の空白文字 が代入された 2次元配列の list なので、それを作成するプログラムは以下のように記述できます。

先程のプログラムよりは単純なプログラムなので、先ほどのプログラムが理解できれば特に難しい点はないでしょう。

board = [ [ " " for y in range(3)] for x in range(3) ]
print(board)

実行結果

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

繰り返し処理の便利な所は、繰り返す数を表す数値を 変更するだけ で、プログラムの他の部分を 一切変更することなく様々な回数 の繰り返し処理を行うことができる点にあります。

例えば、先ほどのプログラムの、繰り返しの回数を表す range() の中の数字を 3 から 5 に変えるだけ で、下記のプログラムのように、5 x 5 のゲーム盤を初期化したデータを表す list を記述することができます。

board = [ [ " " for y in range(5)] for x in range(5) ]
print(board)

実行結果(長いので横にスクロールする必要があります)

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

初期化されたゲーム盤を作成するプログラムは、この記述方法でも充分に 簡潔に記述 できているので、この記述方法を採用しても かまわない のですが、Python では上記のプログラムを さらに簡潔に記述 する方法が用意されているので、次はその方法を紹介します。

* 演算子を使った list の作成

Python では、要素に 同じデータ が複数格納された list を、* 演算子 を使ってより簡潔に記述することができます。

具体的には以下のように記述することで、listのデータの 要素* 演算子の後に記述した整数回3だけ繰り返した list が作成されます。

listのデータ * 整数

いくつか具体例を挙げます。実行結果を見ればプログラムの意味はすぐに分かると思います。

print([1] * 5)
print([" "] * 3)

実行結果

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

上記の例では、list の中に 1 つの要素 しかありませんが、複数の要素がある list に対しても同様の演算を行うことができます。そのような場合に行われる処理については後の記事で紹介します。

勘の良い方は、この表記を応用して、[[" "] * 3] * 3 のように記述すれば、初期化されたゲーム盤を表す 2 次元配列の list を記述できるのではないかと思うかもしれません。具体的には、以下のように考えることでそのようなことを思いつくことができます。

  1. 目的のデータは、[[" ", " ", " "], [" ", " ", " "], [" ", " ", " "]] である
  2. 目的のデータは、list の中に [" ", " ", " "] が代入された要素が 3 つ並んだものである
  3. 従って、目的のデータは [[" ", " ", " "]] * 3 のように記述できる
  4. 目的のデータの中にある [" ", " ", " "] は、list の中に " " が代入された要素が 3 つ並んだものである
  5. 従って、[" ", " ", " "][" "] * 3 のように記述できる
  6. 上記の 3 を、5 を使って置き換えると、[[" "] * 3] * 3 になる

実際に [[" "] * 3] * 3 を記述して print でその内容を表示すると、下記のプログラムの実行結果のように、初期化されたゲーム盤を表す 2 次元配列の list が board に代入されているように見えます。

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

実行結果

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

一見するとうまくいっている ように見えます が、実はこの記述方法には 重大な欠陥 があるため採用することはできません。

例えば、以下のように (1, 0) のマスを表す要素に "〇" を代入した後で、board を表示すると以下のように (0, 0)、(1, 0)、(2, 0) のマスを表す 3 つの要素同時に "〇" になってしまいます。

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

実行結果

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

初心者の方は何が起きているかが理解しづらいと思いますが、なぜこのようなことが起きるかについて 正しく理解する ことは、Python のプログラミングを行っていく上で 非常に重要 で、避けて通ることはできません

そこで、〇×ゲームの実装から一旦離れて、次回から数回にわたって何故このような現象がおきるかについて詳しく説明することにします。

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

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

次回の記事

  1. append は「追加」という意味の英単語です

  2. _ は、半角のアンダースコアという記号です。Python の変数名には、記号は _ のみ 使用することができ、_ だけの変数名を使うことができます

  3. 0 以下の整数を指定した場合は、空の list が作られます

1
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
1
0