目次と前回の記事
実装の進捗状況と前回までのおさらい
〇×ゲームの仕様と進捗状況
正方形で区切られた 3 x 3 の 2 次元のゲーム盤上でゲームを行う
ゲーム開始時には、ゲーム盤の全てのマスは空になっている
2 人のプレイヤーが遊ぶゲームであり、一人は 〇 を、もう一人は × のマークを受け持つ
- 2 人のプレイヤーは、交互に空いている好きなマスに自分のマークを 1 つ置く
- 先手は 〇 のプレイヤーである
- プレイヤーがマークを置いた結果、縦、横、斜めのいずれかの一直線の 3 マスに同じマークが並んだ場合、そのマークのプレイヤーの勝利とし、ゲームが終了する
- すべてのマスが埋まった時にゲームの決着がついていない場合は引き分けとする
仕様の進捗状況は、以下のように表記します。
- 実装が完了した部分を
背景が灰色の長方形
で記述する - 実装の一部が完了した部分を、太字 で記述する
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
前回までのおさらい
前回の記事では、モジュールの作成とインポートの方法、docstring 等の書き方について説明しました。
ゲーム盤の表示
ここまでの記事では、ゲーム盤のデータを下記のプログラムのように print
で直接 表示していました。
なお、今回の記事から、marubatsu.py に保存した marubatsu モジュールから 必要な関数をインポート してプログラムを記述します。前回の記事で説明したように、marubatsu.py は 記事ごとに更新していく ので、記事のプログラムを VSCode に 実際に入力して実行 する方は、記事の最初にあるリンク先から これまでに作成したモジュールをダウンロード して下さい。下記のプログラムの 1 行目では、marubatsu モジュールから initialize_board
をインポート しています。
from marubatsu import initialize_board
board = initialize_board()
print(board)
実行結果
[[' ', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
実行結果のような、ゲーム盤のデータを print
で直接表示 するという方法は 直観的ではなく、わかりづらい ので、今回の記事では、ゲーム盤を わかりやすく表示する プログラムを記述することにします。
ゲーム盤の表示は、「その 3」の記事で説明した、UI の出力 に相当する処理です。UI には 文字だけを使う CUI と 画像を使う GUI があります。GUI は CUI に比べて 見栄えが良いという利点 がありますが、実装 するのが CUI に比べて 大変 なので、今回の記事では 実装が簡単 な CUI でゲーム盤の表示 を行うことにします。GUI のゲーム盤の表示は、一通り〇×ゲームが完成してから実装することにします。
CUI でのゲーム盤の表示
CUI は 文字だけ でゲーム盤を 表示 するので、これまで何度も利用してきた、文字列を表示する処理を行う組み込み関数 print
を使って実装 することが出来ます。また、ゲーム盤のデータは、マークを表す 文字列 が要素に代入された 2 次元配列を表す list で表現されているので、ゲーム盤の 全ての要素 を print
で 並べて表示 する方法が考えられるでしょう。
2 次元配列 を表す list の 全ての要素 を print
で表示する処理は、下記のプログラムのように、入れ子にした for 文 による 繰り返し処理 で行うことが出来ます。同様の繰り返し処理については「その6」の記事で説明しましたので、忘れた方は復習して下さい。
# 各列に対する繰り返しの処理を行う
for x in range(3):
# x 列の各マスに対する繰り返しの処理を行う
for y in range(3):
# (x, y) のマスの要素を表示する
print(board[x][y])
実行結果
実行結果を見ると、何も表示されていないように見える かもしれませんが、実際には 半角の空白文字 が 9 行分表示 されています。そのことは、上記の実行結果の黒い部分を マウスでドラッグして選択 することで、下図のように確認することが出来ます。下図の 青い長方形 が、選択された半角の空白文字 です。
このように、空白文字 を print
で表示 すると、何も表示されていないように見える ため、CUI では 空白文字の代わり に 半角の .
(ピリオド)など の別の文字列を表示することが良くあります。そこで、本記事でも、空白のマス に対して、半角の .
を表示することにします。
空白のマスを、別の文字で表示したい人は、自由に変更してもらってもかまいません。お手本通りにプログラムを記述するのではなく、自分でアレンジ を行うことは、プログラミングの上達 にもつながります。
他の for 文を使った繰り返し
上記のプログラムでは、ゲーム盤の行と列の数が 3 であることから、range(3)
を使って for 文の繰り返しの処理を記述しましたが、2 次元配列 を表す list が代入された board
を直接使って for 文の繰り返しの処理を記述することが出来ます。
board
の 3 つの要素 は、ゲーム盤の 各列を表す list なので、下記のように for 文を記述することで、繰り返しのたびに、board
から 各列のデータ を表す list を 取り出して、col
1 という変数に 代入する という処理を記述することが出来ます。
# 繰り返しのたびに、board から各列のデータを取り出して col に代入する
for col in board:
この col
は 列のデータ を表す list なので、この col
に対して下記の 4 行目のような for 文を記述することで、繰り返しのたびに col
から その 列の各マスのデータ を表す文字列を取り出して、cell
という変数に 代入する という処理を行うことが出来ます。そのため、6 行目のように、この 2 重の for 文の繰り返しのブロックで、cell
を表示することで、ゲーム盤の すべてのマスを表示 することが出来ます。
1 # 繰り返しのたびに、board から列のデータを取り出して col に代入する
2 for col in board:
3 # 繰り返しのたびに、col からマスのデータを取り出して cell に代入する
4 for cell in col:
5 # 取り出したマスのデータを表示する
6 print(cell)
行番号のないプログラム
# 繰り返しのたびに、board から列のデータを取り出して col に代入する
for col in board:
# 繰り返しのたびに、col からマスのデータを取り出して cell に代入する
for cell in col:
# 取り出したマスのデータを表示する
print(cell)
実行結果
理由は後で説明しますが、この方法を ゲーム盤を表示する処理に使うことはできない ので、本記事では range
を使う方法を採用 します。
空白のマスに .
を表示する方法
ゲーム盤を表示する際に、空白のマスを 半角の .
で表示する方法は いくつかあります が、本記事ではその中の 2 つの方法を紹介します。
if 文を使う方法
if 文 を使って、下記の 7~11 行目 のプログラムを記述することで、空白のマスに対して半角の .
を表示 することが出来ます。マークが配置されていないマス を表す要素には 半角の空白文字が代入 されているので、7 行目の if 文の 条件式 で (x, y) のマスが 空であるかどうかを判定 し、その場合は 8 行目で半角の "."
を、そうでなければ 11 行目で (x, y) のマスの要素の値をそのまま表示しています。
実行結果から 9 つの半角の .
が表示されることを確認することが出来ます。
1 # 各列に対する繰り返しの処理を行う
2 for x in range(3):
3 # x 列の各マスに対する繰り返しの処理を行う
4 for y in range(3):
5 # (x, y) のマスの要素を表示する
6 # 空白のマスの場合は . を表示する
7 if board[x][y] == " ":
8 print(".")
9 # そうでなければ、(x, y) のマスの要素をそのまま表示する
10 else:
11 print(board[x][y])
行番号のないプログラム
# 各列に対する繰り返しの処理を行う
for x in range(3):
# x 列の各マスに対する繰り返しの処理を行う
for y in range(3):
# (x, y) のマスの要素を表示する
# 空白のマスの場合は . を表示する
if board[x][y] == " ":
print(".")
# そうでなければ、(x, y) のマスの要素をそのまま表示する
else:
print(board[x][y])
修正箇所
# 各列に対する繰り返しの処理を行う
for x in range(3):
# x 列の各マスに対する繰り返しの処理を行う
for y in range(3):
# (x, y) のマスの要素を表示する
- print(board[x][y])
+ # 空白のマスの場合は . を表示する
+ if board[x][y] == " ":
+ print(".")
+ # そうでなければ、(x, y) のマスの要素をそのまま表示する
+ else:
+ print(board[x][y])
実行結果
.
.
.
.
.
.
.
.
.
空白のマスを表す文字列を変更する方法
これまでは空白のマスを表す 要素 に 半角の空白文字 を代入していましたが、空白のマスを表す要素に 半角の .
を代入 することで、if 文を使わずに 空白のマスに対して半角の .
を表示することが出来ます。
そのためには、下記のプログラムのように initialize_board
を修正 する必要があります。修正した内容は docstring の説明文と、return 文 の " "
を "."
に変更 しただけです。具体的な修正を行った行については、下記の修正箇所をクリックして見て下さい。
関数を修正する際に、docstring の修正箇所も表記すると長くなるので、以降は 関数の修正 の際に docstring は省略 します。修正した docstring については記事の最後の marubatsu_new.py を見て下さい。
def initialize_board() -> list[list[str]]:
""" 初期化されたゲーム盤のデータを返す.
Returns:
初期化されたゲーム盤を表す 2 次元配列の list.
全ての要素に半角の "." が代入されている.
"""
return [["."] * 3 for x in range(3)]
修正箇所
def initialize_board() -> list[list[str]]:
""" 初期化されたゲーム盤のデータを返す.
Returns:
初期化されたゲーム盤を表す 2 次元配列の list.
- 全ての要素に半角の空白文字が代入されている.
+ 全ての要素に半角の "." が代入されている.
"""
- return [[" "] * 3 for x in range(3)]
+ return [["."] * 3 for x in range(3)]
from marubatsu import initialize_board
を実行した後で、上記のプログラムのように インポートした initialize_board
と 同じ名前の関数の定義 を実行すると、新しく定義した関数 で、initialize_board
が上書き されます。このように、既に定義 されている関数と 同じ名前の関数の定義を実行 して上書きすることを、関数の再定義 と呼びます。
修正した initialize_board
と range
を使った for 文による繰り返し処理を記述することで、下記のプログラムのように、if 文を使うことなく、空白のマスに 対して 半角の .
を表示 することが出来ます。
board = initialize_board()
# 各列に対する繰り返しの処理を行う
for x in range(3):
# x 列の各マスに対する繰り返しの処理を行う
for y in range(3):
# (x, y) のマスの要素を表示する
print(board[x][y])
実行結果
.
.
.
.
.
.
.
.
.
こちらのほうがプログラムの変更が少ないので、本記事ではこちらの方法を採用 することにします。
print で改行を行わない方法
上記のプログラムでは、マスのデータを 表示するたび に 改行 が行われますが、それは print
が文字列を表示した後で、自動的に改行を行う からです。
print
は、下記のプログラムの 8 行目のように、実引数の最後に end=""
を記述することで、改行を行わずに 文字列を表示することが出来ます。
1 board = initialize_board()
2
3 # 各列に対する繰り返しの処理を行う
4 for x in range(3):
5 # x 列の各マスに対する繰り返しの処理を行う
6 for y in range(3):
7 # (x, y) のマスの要素を改行せずに表示する
8 print(board[x][y], end="")
行番号のないプログラム
board = initialize_board()
# 各列に対する繰り返しの処理を行う
for x in range(3):
# x 列の各マスに対する繰り返しの処理を行う
for y in range(3):
# (x, y) のマスの要素を改行せずに表示する
print(board[x][y], end="")
修正箇所
board = initialize_board()
# 各列に対する繰り返しの処理を行う
for x in range(3):
# x 列の各マスに対する繰り返しの処理を行う
for y in range(3):
# (x, y) のマスの要素を改行せずに表示する
- print(board[x][y])
+ print(board[x][y], end="")
実行結果
.........
print
の 最後の実引数 に、end=""
を記述 することで、改行を行わずに文字列を表示 することが出来る。
この end=""
の意味について正しく理解するためには、Python の 位置引数、キーワード引数、デフォルト引数 について理解する必要があります。
位置引数とキーワード引数
これまでのプログラムでは、関数呼び出しを行う際に記述する 実引数 は、関数の定義に記述された 仮引数の順番と対応 して記述していました。このような、仮引数の順番と対応 して記述する 実引数 の事を、仮引数の 位置に対応 した場所に記述することから、位置引数 と呼びます。
多くの仮引数 を持つ関数に対して、位置引数 として 実引数を記述 すると、どのような 順番 で実引数を記述するかや、記述した実引数が 何のデータを表しているか が わかりづらくなる という欠点があります。そのような場合は、実引数を 仮引数名=実引数
のように記述して呼び出すという方法が便利です。この方法では、実引数を 仮引数の名前 という キーワード を使って 指定 するので、このような方法で記述された実引数のことを、キーワード引数 と呼びます。
具体例を挙げます。これまでに作成した、place_mark
という関数の仮引数は、先頭から順番に board
、x
、y
、mark
という名前が付けられています。下記のプログラムは、この関数呼び出しの実引数をキーワード引数で記述したものです。なお、今回の記事ではまだ place_mark
をインポートしていないので、1 行目でインポートしています。
下記のプログラムの 4 行目のように、キーワード引数 を使うことで、関数の定義に記述された 仮引数の順番 とは 異なる順番 で、実引数を記述 することが出来ます。また、それぞれの実引数が 何のデータ を表しているかが 明確 になります。
実引数 が何のデータを表しているかを 明確にする ために、すべての実引数の位置 が仮引数と 同じ場合でも、キーワード引数 を使って 実引数を記述 する場合が 良くあります。
from marubatsu import place_mark
board = initialize_board()
place_mark(mark="〇", y=1, x=0, board=board)
print(board)
実行結果
( 0 , 1 ) のマスにはマークが配置済です
[['.', '.', '.'], ['.', '.', '.'], ['.', '.', '.']]
キーワード引数を記述する際にわかりづらいのは、上記の board=board
のように、変数に同じ名前の変数を代入 するという、一見すると意味のない 記述を行っているように 見える 部分です。
キーワード引数では、関数呼び出しの際に行われる、仮引数に実引数を代入 するという 処理を記述 します。従って、キーワード引数の =
の前 に記述されているのは、関数の ローカル変数 である 仮引数、=
の後ろ に記述されているのは グローバル変数 なので、この 2 つは 同じ名前 であっても 異なる変数 です。
キーワード引数を記述する際に良く見られる、board=board
のような記述は、ローカル変数 である 仮引数 board
に、グローバル変数 である 実引数 board
を 代入する処理 を表す。
上記のプログラムのバグ
上記のプログラムを実行すると、実行結果に「( 0 , 1 ) のマスにはマークが配置済です」が表示されてしまいます。これは、先ほど マークが配置されていないマス を表す文字列を 半角の空白文字 から 半角の .
に 変更 したにも関わらず、place_mark
の処理で、(x, y) のマスに マークが配置されていない ことを 判定する if 文 を 変更していない ことが原因です。
このように、プログラムで 何かを変更 した場合は、その変更した内容に 関連する部分をすべて修正 する必要があり、その 修正を忘れる と上記のような バグが発生してしまう 点に注意が必要です。
下記のプログラムのように、place_mark
の 2 行目の if 文の " "
を "."
に修正 することで、このバグを修正することができます。実際に実行結果にマークが配置済というメッセージが表示されなくなります。
1 def place_mark(board: list[list[str]], x: int, y: int , mark: str):
2 if board[x][y] == ".":
3 board[x][y] = mark
4 else:
5 print("(", x, ",", y, ") のマスにはマークが配置済です")
6
7 board = initialize_board()
8 place_mark(mark="〇", y=1, x=0, board=board)
9 print(board)
行番号のないプログラム
def place_mark(board: list[list[str]], x: int, y: int , mark: str):
if board[x][y] == ".":
board[x][y] = mark
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
board = initialize_board()
place_mark(mark="〇", y=1, x=0, board=board)
print(board)
修正箇所
def place_mark(board: list[list[str]], x: int, y: int , mark: str):
- if board[x][y] == " ":
+ if board[x][y] == ".":
board[x][y] = mark
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
board = initialize_board()
place_mark(mark="〇", y=1, x=0, board=board)
print(board)
実行結果
[['.', '〇', '.'], ['.', '.', '.'], ['.', '.', '.']]
位置引数とキーワード引数の両方を使う場合
関数呼び出しを記述する際に、位置引数 と キーワード引数 の 両方を使う こともできます。ただし、その場合は、必ず すべての位置引数 を キーワード引数よりも先 に記述する必要があります。下記のプログラムはそのように記述されているので正しく動作します。
board = initialize_board()
place_mark(board, 0, mark="〇", y=1)
print(board)
実行結果
[['.', '〇', '.'], ['.', '.', '.'], ['.', '.', '.']]
一方、下記のプログラムは、位置引数 よりも前に キーワード引数を記述しているので、下記の実行結果のように、実行すると エラーが発生 します。
board = initialize_board()
place_mark(y=1, 0, mark="〇", board=board)
print(board)
実行結果
Cell In[11], line 2
place_mark(y=1, 0, mark="〇", board=board)
^
SyntaxError: positional argument follows keyword argument
上記のエラーメッセージは、以下のような意味を持ちます。
-
SyntaxError
構文(文法のこと)(syntax)に関するエラー -
positional argument follows keyword argument
位置引数(positional argument)が、キーワード引数(keyword argument)の後(follows)に記述されている
デフォルト引数とデフォルト引数値
関数の仮引数には、デフォルト引数値2 という、実引数を記述しなかった場合 に 代入される値 を設定することが出来ます。デフォルト引数値は、関数の定義の際に 仮引数名=デフォルト引数値
のように記述します。また、デフォルト引数値が設定された仮引数の事を、デフォルト引数 と呼びます。
デフォルト引数 と デフォルト引数値 は名前が似ていて 紛らわしい ので、本記事では以降は、デフォルト引数値 の事を デフォルト値 と表記します。
下記のプログラムは、1、2 行目で、3 つの仮引数の合計を計算して返り値として返す add
という関数を定義しています。3 つの仮引数のうちの最後の仮引数 z
に z=0
を記述 することで、関数呼び出しの際に、z
対応する 実引数を記述しなかった場合 は、z
に 0
が代入 されて関数の処理が行われます。
この関数の 仮引数 z
は デフォルト引数 で、その デフォルト値は 0
です。
def add(x, y, z=0):
return x + y + z
print(add(1, 2))
print(add(1, 2, 3))
実行結果
3
6
上記のプログラムの 4 行目では、デフォルト引数 z
に対応 する 実引数を記述せず に関数呼び出しを行っているので、z
に 0
が代入 されて処理が行われた結果、1 + 2 + 0
が計算されて 3
が返り値として返ります。
上記のプログラムの 5 行目では、デフォルト引数 z
に対応 する 実引数に 3
を記述 して関数呼び出しを行っているので、z
に 3
が代入 されて処理が行われた結果、1 + 2 + 3
が計算されて 6
が返り値として返ります。
上記のプログラムでは、2 つまたは 3 つの実引数の合計を計算する関数を定義していますが、この方法では、任意の数の仮引数 の合計を計算する関数を定義する事は できません。任意の数の仮引数を扱う関数は、デフォルト引数とは 別の方法 で定義することが出来ます。その方法については、必要になった時点で説明します。
デフォルト引数は、デフォルト値を指定しない 通常の仮引数より後 に記述する必要があります。例えば、下記のプログラムの add
では、デフォルト値を指定しない 通常の仮引数 y
が、デフォルト引数 x
より後 に記述されているので、実行すると エラーが発生 します。
def add(x=0, y, z=0):
return x + y + z
実行結果
Cell In[13], line 1
def add(x=0, y, z=0):
^
SyntaxError: non-default argument follows default argument
上記のエラーメッセージは、以下のような意味を持ちます。
-
SyntaxError
構文(文法のこと)(syntax)に関するエラー -
non-default argument follows default argument
デフォルト引数でない仮引数(non-default argument)が、デフォルト引数(default argument)の後(follows)に記述されている
キーワード引数とデフォルト引数
デフォルト引数が複数記述 されている関数に対して、途中のデフォルト引数 に対応する 実引数を省略 して関数呼び出しを記述する場合は、キーワード引数を利用する必要 があります。
言葉の説明ではわかりづらいと思いますので、具体例を挙げます。下記のプログラムは、5 つの仮引数の値を print
で表示するという処理を行う print_data
という関数を定義しています。この関数は、5 つの仮引数のうち、c
と d
と e
はデフォルト引数 で、いずれも デフォルト値は None
です。
この関数に対する関数呼び出しの際に、c
と d
に対応する実引数を省略 する場合は、下記のプログラムの 4 行目のように、省略しない e
に対応する実引数 は キーワード引数で記述する必要 があります。
def print_data(a, b, c=None, d=None, e=None):
print("a =", a, "b =", b, "c =", c, "d =", d, "e =", e)
print_data(1, 2, e=5)
実行結果
a = 1 b = 2 c = None d = None e = 5
上記のように記述する必要がある理由は、下記のプログラムのように、キーワード引数を使わず に 3 つ目の実引数を記述 した場合は、3 つ目の実引数は 位置引数 になり、仮引数 c
に対応 する実引数だとみなされてしまうからです。下記のプログラムの場合は、実行結果からわかるように、d
と e
に対応する実引数が 省略 されたと みなされます。
print_data(1, 2, 5)
実行結果
a = 1 b = 2 c = 5 d = None e = None
デフォルト引数に関する注意点
デフォルト引数の デフォルト値に、list などの ミュータブルなデータを記述 すると、意図しない処理 が行われる 可能性がある ので、避けたほうが良い でしょう。
その理由については複雑な話になるので、下記の説明の意味が分からない場合は 読み飛ばしてもらっても構いません。ただし、読み飛ばす場合でも、理屈はわからなくても構わないので、意図しない処理が行われないよう に、デフォルト値 には、数値型、文字列型、論理型、None 型 のいずれかのデータを記述するように気を付けて下さい。
より正確には、「その 11」の記事で説明した、疑似的な複製 が行われるデータをデフォルト値にすれば、意図しない処理が行われることはありません。
参考までに、このことに関する Python の公式ドキュメントへのリンクを挙げておきます。
デフォルト値に関するよくある勘違い
デフォルト値は、常に 関数の定義で記述した値であると 勘違いされがち ですが、実際には、デフォルト値 は関数呼び出しが行われると 変化する場合 があります。
具体例を挙げて説明します。下記のプログラムの append_list
は、仮引数 alist
に代入された list の要素に、仮引数 data
に代入されたデータを追加し、alist を返り値として返すという処理を行う関数です。
仮引数の名前を alist
にしたのは、list
という名前の組み込み関数が存在する ためです。alist
は、関数のブロックの中で要素を追加(append)する list であることから、そのような名前を付けました。わかりにくいと思った方は、別の名前に変更してもかまいません。
デフォルト引数である alist
の デフォルト値 には、空の list が記述 されているので、この関数で以下のような処理が行われると 勘違いする 人が多いのではないでしょうか?
下記の説明は 間違っている ので背景が赤いノートに記述しています。
- 仮引数
alist
に対応 する実引数を 記述した場合 は、alist.append(data)
が実行され、alist
の要素にdata
が追加された list が 返り値として返る - 仮引数
alist
に対応 する実引数を 省略した場合 は、alist
に デフォルト値である 空の list が代入 される。alist.append(data)
が実行され、data
のみを要素とする[data]
が返り値として返る
1 def append_list(data, alist=[]):
2 alist.append(data)
3 return alist
4
5 print(append_list(2, [0, 1])) # alist に対応する実引数を記述した場合
6 print(append_list("c", ["a", "b"]))
7 print(append_list(1)) # alist に対応する実引数を省略した場合
8 print(append_list(2))
行番号のないプログラム
def append_list(data, alist=[]):
alist.append(data)
return alist
print(append_list(2, [0, 1])) # alist に対応する実引数を記述した場合
print(append_list("c", ["a", "b"]))
print(append_list(1)) # alist に対応する実引数を省略した場合
print(append_list(2))
実行結果
[0, 1, 2]
['a', 'b', 'c']
[1]
[1, 2]
上記のプログラムの 5 行目では、alist
に対応 する実引数を 記述 しており、その実引数の [0, 1]
の要素 に、data
に対応する実引数の 2
が追加されるので、実行結果は [0, 1, 2]
となります。
6 行目でも、alist
に対応 する実引数を 記述 しており、同様の処理が行われます。このように、alist
に対応 する実引数を 記述 した場合の処理は、先ほどのノートで 説明した通りの処理 が行われます。
7 行目では、alist
に対応 する実引数を 省略 しており、空の list の要素 に data
に対応する実引数の 1
が追加されるので、実行結果は [1]
となります。
8 行目でも、alist
に対応 する実引数を 省略 しており、空の list の要素に data
に対応する実引数の 2
が追加されるので、実行結果は [2]
となると思うかもしれませんが、実際には [1, 2]
になります。このことから、先ほどの alist
に対応 する実引数を 省略 した場合の 処理の説明 は、間違っている ことがわかります。
デフォルト引数が記述された関数の定義で行われる処理
デフォルト引数が記述された関数の定義を実行すると、以下のような処理が行われます。なお、この処理は、関数呼び出し ではなく、関数の定義が実行 された時に行われる処理である点に注意して下さい。
- 関数の定義 を管理する オブジェクトが作成 される
- デフォルト値 を管理する オブジェクトが作成 される
- 関数の定義 を管理する オブジェクトの中 に、デフォルト引数の名前 から デフォルト値 を管理する オブジェクトへの対応づけ が 記録 される
上記の手順 3 から、関数の定義を実行すると、関数の定義を管理するオブジェクトの中に、デフォルト引数名 から デフォルト値 を管理するオブジェクトを 対応づける名前空間が記録 されます。この名前空間は、関数呼び出しの際に、デフォルト引数に対応する 実引数が省略 された 場合でのみ利用 されます。
下図は、先程の append_list
の定義を実行した際に作成されるオブジェクトです。
7 行目の append_list(1)
で行われる処理
関数呼び出しの際に、デフォルト引数に対応する 実引数を記述しなかった場合 は、デフォルト引数は、その関数の定義の中で デフォルト引数名に対応づけられたオブジェクトを参照 します。
言葉の説明では意味が分かりづらいと思いますので、先ほどのプログラムの 7 行目の append_list(1)
の関数呼び出しを行った 直後の状態 を下図に示します。図のように、append_list
の ローカル変数 alist
は、append_list
の定義 を管理する オブジェクトの中 の、alist
に対応づけられた オブジェクトを 参照 します。
この後で、append_list
のブロック内の alist.append(data)
が実行されると、下図のような状態になります。
図からわかるように、append_list
の定義 を管理する オブジェクトの 中の、alist
に対応づけられた オブジェクトが管理する データ は、空の list から、[1]
に変化 します。
8 行目の append_list(2)
で行われる処理
この状態で、次の 8 行目の append_list(2)
の関数呼び出しを実行すると、append_list
の ローカル変数 alist
には、空の list ではなく、[1]
が代入 されます。そのため、append_list
のブロック内の alist.append(data)
が実行されると、alist
に代入されたデータは [1, 2]
になります。
このような現象は、デフォルト値がデータの中身を後から変更できる ミュータブルなデータ の場合に発生します。従って、データの中身を変更することができない、数値型、文字列型、論理型、None 型 のデータなどの、疑似的な複製3が行われるデータでは 発生しません。
下記のプログラムを記述することで、上記の append_list
を、「alist
に対応 する実引数を 省略 した場合は、data
に対応する 実引数のみ を要素として持つ list を返す」ように 修正する ことが出来ます。
下記のプログラムは、1 行目で、alist
のデフォルト値を None
に変更しています。None は 疑似的な複製 が行われるデータなので、alist
に対応する実引数が 省略 されて append_list
が呼び出された場合は、alist
には 常に None
が代入 されます。
2 行目の if 文で alist
が None
であることを判定 し、その場合のみ 3 行目で alist
に空の list を代入 しています。なお、これまでの記事で説明していませんでしたが、データが None
であるかどうかを判定 する場合は、==
演算子ではなく、is
演算子を記述する必要 があります。is
演算子の詳細については、必要になった時点で説明します。
先程の場合と異なり、10 行目の実行結果が [2]
になっていることを確認して下さい。
1 def append_list(data, alist=None):
2 if alist is None:
3 alist = []
4 alist.append(data)
5 return alist
6
7 print(append_list(2, [0, 1])) # alist に対応する実引数を記述した場合
8 print(append_list("c", ["a", "b"]))
9 print(append_list(1)) # alist に対応する実引数を省略した場合
10 print(append_list(2))
行番号のないプログラム
def append_list(data, alist=None):
if alist is None:
alist = []
alist.append(data)
return alist
print(append_list(2, [0, 1])) # alist に対応する実引数を記述した場合
print(append_list("c", ["a", "b"]))
print(append_list(1)) # alist に対応する実引数を省略した場合
print(append_list(2))
修正箇所
- def append_list(data, alist=[]):
+ def append_list(data, alist=None):
+ if alist is None:
+ alist = []
alist.append(data)
return alist
print(append_list(2, [0, 1])) # # alist に対応する実引数を記述した場合
print(append_list("c", ["a", "b"]))
print(append_list(1)) # # alist に対応する実引数を省略した場合
print(append_list(2))
実行結果
[0, 1, 2]
['a', 'b', 'c']
[1]
[2]
print
の仮引数 end
の意味
print
には、end
という、print
で文字列を表示した後に 追加して表示する文字列 を表す仮引数があります。end
はデフォルト引数 になっており、その デフォルト値 は 改行を表す文字列 になっています。そのため、print
に対して関数呼び出しを行う際に、実引数に キーワード引数で end
を記述しない 場合は、文字列を表示した後で 改行が行われます。
従って、print
の実引数に end=""
(空文字)を記述することで、文字列を表示した後で 何も表示が追加されなくなる ので、改行が行われなくなります。
もちろん、end
には 他の文字列を指定 することもできます。下記のプログラムでは、print
の実引数に end="."
を記述することで、最後に半角の .
を表示しています。
print("abc", end=".")
実行結果
abc.
まとめ
位置引数に関するまとめ
- 位置引数 とは、仮引数に対応 する 位置 に記述する 実引数 のことである
キーワード引数に関するまとめ
-
キーワード引数 とは、
仮引数名=実引数
のように記述する 実引数 のことである - キーワード引数は、仮引数の位置 とは 無関係な場所 に記述できる
- キーワード引数は、すべて の 位置引数より後に記述 する必要がある
デフォルト引数とデフォルト値(デフォルト引数値)に関するまとめ
- デフォルト引数 とは、関数呼び出しの際に 対応する実引数を省略 できる 仮引数 のことである
- デフォルト引数は、関数の定義で
仮引数名=デフォルト値
のように記述する - 関数呼び出しの際に、デフォルト引数に対応する 実引数が省略 された場合は、仮引数にはデフォルト値が代入 される
- デフォルト引数は、すべて の 通常の引数 より 後に記述 する必要がある
- 関数呼び出しの際に、途中の デフォルト引数に対応する実引数を 省略 する場合は、省略しない デフォルト引数に対応する実引数を キーワード引数で記述する必要 がある
- デフォルト引数の仕組みを正しく理解していない場合は、デフォルト値には 数値型、文字列型、論理型、None 型 のいずれかのデータ型のみを記述したほうが良い
ゲーム盤の行ごとに改行する方法
先程のプログラムでは、マスを表示する print
の実引数に end=""
を記述したので、.
が横に 9 つ並んで表示 が行われました。次は、ゲーム盤の行ごとに改行 するようにプログラムを修正します。
改行は、各列のマスに対する繰り返し を行う 内側の for 文 が 終了した直後 で行えばよいので、下記のプログラムのように 10 行目に print()
を追加 します。なお、文字列を表示せずに 改行だけを行いたい 場合は、print()
のように、実引数に何も記述せず に print
を呼び出します4。
実行結果から、全てのマスが空の 3 x 3 のゲーム盤 がうまく表示されているように見えます。
1 board = initialize_board()
2
3 # 各列に対する繰り返しの処理を行う
4 for x in range(3):
5 # x 列の各マスに対する繰り返しの処理を行う
6 for y in range(3):
7 # (x, y) のマスの要素を改行せずに表示する
8 print(board[x][y], end="")
9 # x 列の表示が終わったので、改行する
10 print()
行番号のないプログラム
board = initialize_board()
# 各列に対する繰り返しの処理を行う
for x in range(3):
# x 列の各マスに対する繰り返しの処理を行う
for y in range(3):
# (x, y) のマスの要素を改行せずに表示する
print(board[x][y], end="")
# x 列の表示が終わったので、改行する
print()
修正箇所
board = initialize_board()
# 各列に対する繰り返しの処理を行う
for x in range(3):
# x 列の各マスに対する繰り返しの処理を行う
for y in range(3):
# (x, y) のマスの要素を改行せずに表示する
print(board[x][y], end="")
+ # x 列の表示が終わったので、改行する
+ print()
実行結果
...
...
...
ゲーム盤の表示を行う関数の定義
ゲーム盤の表示は、今後の〇×ゲームを実装する際に、様々な場面で行う可能性が高い処理 なので、ゲーム盤の表示を行う 関数を定義 することにします。
関数を定義するためには、関数の 名前、仮引数、関数が行う 処理、返り値 を決める必要があるので、それぞれについて、以下のように決めることにします
-
関数の名前 はゲーム盤(board)を表示(display)するので、
display_board
とする -
仮引数 は 表示するゲーム盤のデータ を代入する
board
とする -
関数の処理 は、
board
の内容を表示する - 返り値 は必要がないので、返り値を返さない関数とする
関数が行う処理のプログラムは 先ほどのプログラムと同じ なので、display_board
は下記のプログラムのように定義することが出来ます
def display_board(board: list[list[str]]):
# 各列に対する繰り返しの処理を行う
for x in range(3):
# x 列の各マスに対する繰り返しの処理を行う
for y in range(3):
# (x, y) のマスの要素を改行せずに表示する
print(board[x][y], end="")
# x 列の表示が終わったので、改行する
print()
display_board
の処理の確認
display_board
が 正しく動作するか を、ゲーム盤に マークを配置 して 確認 することにします。
下記のプログラムは、(0, 1) のマスに 〇 を配置 してゲーム盤を表示するプログラムです。
board = initialize_board()
place_mark(board, 0, 1, "〇")
display_board(board)
実行結果
.〇.
...
...
実行結果から display_board
には、以下のような 問題点 がある事がわかります。
- (0, 1) ではなく、(1, 0) のマス に 〇 が表示される
- 〇 を表示した結果、上下の行で表示位置がずれる
マークの表示位置がおかしい原因
プログラムは、記述した内容通りの処理が必ず実行 されます。従って、プログラムで 意図しない処理、すなわち バグが発生 した場合は、必ず 何らかの原因 があります。バグの原因を見つける 方法の一つ に、意図しない処理 が行われるプログラムを 複数回実行 することで、意図しない処理が行われる 法則を見つけ出し、その 法則を元に バグの 原因を推測する という方法があります。
そこで、どのような 法則 でマークが表示される 位置がずれる かを調べるために、もう一つ (1, 2) のマスに × のマークを配置 してみることにします。
board = initialize_board()
place_mark(board, 0, 1, "〇")
place_mark(board, 1, 2, "×")
display_board(board)
実行結果
.〇.
..×
...
今度は、(1, 2) ではなく、(2, 1) のマスに × が表示されることがわかります。
2 つの 位置のずれ を 比較する と、x 座標と y 座標が逆 になっていることがわかります。そのことを念頭に置いて、display_board
のプログラムを見てみると、下記の部分が おかしい ことがわかります。
# 各列に対する繰り返しの処理を行う
for x in range(3):
# x 列の各マスに対する繰り返しの処理を行う
for y in range(3):
# (x, y) のマスの要素を改行せずに表示する
今回は 運よく、すぐに法則が見つかりましたが、なかなか法則が見つからない場合は、さらにいくつかの処理を実行する必要があります。また、この方法では バグの原因を見つけられない場合もある ので、見つからない場合はあきらめて 別の方法 でバグの原因を探す必要があります。
上記のプログラムは、外側の for 文 で 各列 の繰り返しの処理を行い、内側の for 文 で その列の各マス に対する繰り返し処理を行っています。このことから、このプログラムで表示される文字は、0 列、1 列、2 列 の順番で表示が行われることがわかります。
実際には、0 行、1 行、2 行 の順番で表示を行いたいので、for 文の 入れ子の順番が間違っている ことがこのバグの原因です。下記は、for 文の 入れ子の順番を入れ替えた プログラムで、実行するとマークが正しい位置に表示されるようになります。
def display_board(board: list[list[str]]):
# 各行に対する繰り返しの処理を行う
for y in range(3):
# y 行の各マスに対する繰り返しの処理を行う
for x in range(3):
# (x, y) のマスの要素を改行せずに表示する
print(board[x][y], end="")
# y 行の表示が終わったので、改行する
print()
board = initialize_board()
place_mark(board, 0, 1, "〇")
place_mark(board, 1, 2, "×")
display_board(board)
修正箇所
def display_board(board: list[list[str]]):
- # 各列に対する繰り返しの処理を行う
+ # 各行に対する繰り返しの処理を行う
- for x in range(3):
+ for y in range(3):
- # x 列の各マスに対する繰り返しの処理を行う
+ # y 行の各マスに対する繰り返しの処理を行う
- for y in range(3):
+ for x in range(3):
# (x, y) のマスの要素を改行せずに表示する
print(board[x][y], end="")
# y 行の表示が終わったので、改行する
print()
board = initialize_board()
place_mark(board, 0, 1, "〇")
place_mark(board, 1, 2, "×")
display_board(board)
実行結果
...
〇..
.×.
2 次元配列を表す list に対して、for 文を入れ子 にして繰り返しの処理を行う場合は、入れ子の順番が重要 になる場合があるので、その点に注意して プログラムを記述する必要がある。
入れ子の順番が 重要でない処理 を行う場合は、どの順番で for 文を記述してもかまわない。
ゲーム盤の表示に board
に対する繰り返し処理が使えない理由
今回の記事の前半で、2 次元配列を表す list が代入 された board
に対して for 文を記述 する繰り返し処理について紹介しましたが、この方法で〇×ゲームのゲーム盤を 表示することはできません。その理由は、board
の 1 つめのインデックス が 参照する要素 は、各列 のデータを表す要素だからです。下記のように記述すると、外側の for 文 は 列に対する繰り返し になるので、先ほどと同じ理由で、行と列が入れ替わった状態 でゲーム盤が 表示 されてしまいます。
board = initialize_board()
place_mark(board, 0, 1, "〇")
place_mark(board, 1, 2, "×")
# 繰り返しのたびに、board から列のデータを取り出して col に代入する
for col in board:
# 繰り返しのたびに、col からマスのデータを取り出して cell に代入する
for cell in col:
# 取り出したマスのデータを改行せずに表示する
print(cell, end="")
# 列の表示が終わったので、改行する
print()
実行結果
.〇.
..×
...
board
の 1 つ目のインデックス がゲーム盤の y 座標 を表すようにすれば、上記のプログラムで ゲーム盤を 正しく表示することが出来る ようになります。ただし、そのように変更すると、数学の座標の記法と逆 に、インデックスが前から順に y 座標、x 座標を表すという 欠点 が生じます。
2 種類の for 文の違いに関する補足
今回の記事で説明したように、2 次元配列を表す list の 全ての要素 に対して処理を行う 繰り返し処理 は、range
を使う方法 と、2 次元配列を表す list そのものを使う方法 があります。
range
を使う方法 は、2 次元配列を表す list の 2 つのインデックス の すべての組み合わせ に対して繰り返しの処理を行うという方法です。この方法には以下のような利点と欠点があります。
-
利点
- 繰り返しの処理の中で、インデックスの番号が必要になる ような処理を記述することが出来る
- 2 つの for 文が どちらのインデックス に対して繰り返しを行うかを 選択する ことが出来る
-
欠点
- list の要素に対して、2 つのインデックスを記述 する必要があるのでプログラムが 少し長くなる
-
range
の引数 に 繰り返す数 を表す 正しい数値 を記述する必要がある
2 次元配列を表す list そのものを使って行う繰り返し処理は、以下のような利点と欠点があります。
-
利点
- 繰り返しの処理の中で、インデックスを使わず に list の要素に対する処理を 短く 記述することが出来る
- 何回繰り返すかを表す数値 を記述する 必要がない
-
欠点
- 繰り返しの処理の中で、インデックスの番号が必要になる ような処理を 記述することが出来ない
- 2 つの for 文が どちらのインデックス に対して繰り返しを行うかを 選択することができない
それぞれの利点と欠点を 比べてみる とわかるように、それぞれの 利点と欠点 は お互いに対応 しています。そのため、どちらの方法を使うかは、これらの 利点と欠点を考慮 して、記述するプログラムで 行う処理 に合わせて 自分で選択する 必要があります。
以下に、それぞれの利点について、ゲーム盤の表示を行うプログラムで比較 しながら説明します。
インデックスの番号が必要になる処理
range
を使った繰り返し処理 では、繰り返しの中で インデックスの番号 が x
と y
に代入 されるので、それを使って下記のプログラムのように、繰り返しの中で マスの座標を表示 することが出来ます。
board = initialize_board()
# 各行に対する繰り返しの処理を行う
for y in range(3):
# y 行の各マスに対する繰り返しの処理を行う
for x in range(3):
# (x, y) のマスの要素を改行せずに表示する
print("(", x, ",", y, ")", board[x][y])
実行結果
( 0 , 0 ) .
( 1 , 0 ) .
( 2 , 0 ) .
( 0 , 1 ) .
( 1 , 1 ) .
( 2 , 1 ) .
( 0 , 2 ) .
( 1 , 2 ) .
( 2 , 2 ) .
一方、2 次元配列を表す list そのものを使って行う繰り返し処理では、繰り返しの中で col
と cell
に代入されるのは、list の要素 なので、繰り返しの中でインデックスの番号を利用する、上記のようなプログラムを 記述することはできません。
細かい話になりますが、実際には、本記事でまだ紹介していない組み込み関数 enumerate
を使えば、2 次元配列を表す list そのものを使って行う繰り返し処理の中で、インデックスの番号を利用することが出来ます。enumerate
に関しては、必要になった時点で紹介します。
インデックスに対応する繰り返し処理の順番の選択
range
を使った繰り返し処理 の場合は、入れ子 になった それぞれの for 文 が、どちらのインデックスに対応 する処理を行うかを 選択 することが出来ます。
具体的には、2 次元配列の list が代入された board
の 全ての要素 に対して処理を行うプログラムは、以下の 2 通りの方法で記述 することが出来ます。
ゲーム盤の表示のように、for 文の 入れ子の順番が重要 になる場合は、正しい順番で記述する 必要があります。
for x in range(3):
for y in range(3):
(x, y) の要素に対する処理
for y in range(3):
for x in range(3):
(x, y) の要素に対する処理
一方、2 次元配列を表す list そのものを使って行う繰り返し処理では、入れ子になった for 文の順番 は、必ず list の インデックスの順番に対応 することになります。
board
の場合は、list の 2 つのインデックスは 順に「x 座標」、「y 座標」 を表すことに 決めた ので、入れ子になった for 文は 外側から順に、「x 座標」に対する 繰り返し、「y 座標」に対する 繰り返し しか行うことが出来ません。
従って、その逆の順番 で for 文を記述する必要があるゲーム盤の表示処理を、2 次元配列を表す list そのものを使って行う繰り返し処理で 記述することはできません。
繰り返しの中でのインデックスの必要性
2 次元配列を表す list そのものを使って行う繰り返し処理では、下記のプログラムのように、繰り返しの中で インデックスを記述する必要 が ない ので、プログラムを 短く記述 できます。
for col in board:
for cell in col:
print(cell)
一方、range
を使って行う繰り返し処理では、下記のプログラムのように、繰り返しの中で 2 つのインデックスを記述する必要 が ある ので、プログラムが 長く なります。
for y in range(3):
for x in range(3):
print(board[x][y])
何回繰り返すかを表す数値の記述
2 次元配列を表す list そのものを使って行う繰り返し処理では、list の要素の数 だけ繰り返しが行われるので、下記のプログラムのように、何回繰り返すかを表す数値 を記述する必要は ありません。
for col in board:
for cell in col:
print(cell)
一方、range
を使って行う繰り返し処理では、下記のプログラムのように、繰り返しの数 を表す 3
という数値を記述する 必要があり、この数値を間違えるとプログラムが正しく動作しなくなります。
for y in range(3):
for x in range(3):
print(board[x][y])
list の 要素の数 を返り値として返す len
という組み込み関数を使うことで、下記のプログラムのように、range
を使って行う繰り返し処理でも、繰り返しの数を記述しなくても済む ようになります。
下記のプログラムでは、2 行目の外側の for 文では 行に対する繰り返し を行います。行の数は、0 列 のデータを表す board[0]
の要素の数に等しいので、range(len(board[0]))
のように記述しています。なお、〇×ゲームのゲーム盤の 行の数 は どの列でも同じ なので、この部分は、range(len(board[1]))
や、range(len(board[2]))
のように記述しても構いません。
# ゲーム盤の行の数は、board[0] の要素の数に等しい
for y in range(len(board[0])):
# ゲーム盤の x 列のマスの数は、board の要素の数に等しい
for x in range(len(board)):
print(board[x][y])
この方法には、「len
を記述する分だけ プログラムが長くなる」、「len
の実引数を 正しく記述する 必要がある」という欠点があります。
文字のずれを解消する方法
上下の行で文字がずれる 原因は、半角 の .
と 全角 の 〇
と ×
を 混在して 表示しているからです。一般的に 全角文字 は、半角文字 よりも 幅が広く表示 されるので上下で表示がずれてしまいます。
また、文字の種類 によって 横幅が異なるフォント5で文字が表示される場合は、全ての文字列を全角文字で表示 した場合でも、上下の行で文字がずれる ことが良くあります6。
下記のプログラムは、ゲーム盤の 0 行に 〇 を、1 行に × を並べて配置したものを表示するプログラムです。実行結果から、全角文字の 〇 と × の 表示幅が異なる ため 上下の行でずれる ことがわかります。
board = initialize_board()
place_mark(board, 0, 0, "〇")
place_mark(board, 1, 0, "〇")
place_mark(board, 2, 0, "〇")
place_mark(board, 0, 1, "×")
place_mark(board, 1, 1, "×")
place_mark(board, 2, 1, "×")
display_board(board)
実行結果
〇〇〇
×××
...
Word などの ワープロ では、文章をきれいに表示するために、一般的に 半角文字 は文字によって 横幅が異なるフォントで表示 されます。一方、プログラムを扱うソフト で 半角文字 を 記述 したり 表示 したりする場合は、一般的に すべての文字が同じ幅 で表示される フォント7が使われます。
実際に、本記事のプログラムを表示する背景が黒い部分でも、VSCode でプログラムを記述したり、print
で文字を表示したりした場合でも、半角文字 が上下の行で ずれて表示される ことは ありません。
そのため、CUI のような、文字だけで出力 を行う際に、上下の行で文字の位置を揃えたい 場合は、すべての文字を半角文字で表示 するのが一般的です。
そこで、以降は 〇 を表す文字 を 半角の小文字の o(オー) に、× を表す文字 を 半角の小文字の x(エックス) に変更することにします。
下記は、そのように変更した場合に、ゲーム盤の 0 行に 〇 を、1 行に × を並べて配置したものを表示するプログラムです。実行結果から、上下の文字がずれなくなった ことを確認することが出来ます。
board = initialize_board()
place_mark(board, 0, 0, "o")
place_mark(board, 1, 0, "o")
place_mark(board, 2, 0, "o")
place_mark(board, 0, 1, "x")
place_mark(board, 1, 1, "x")
place_mark(board, 2, 1, "x")
display_board(board)
実行結果
ooo
xxx
...
CUI で 上下の行で文字を揃えて表示 したい場合は、全ての文字を半角文字 で表示すると良い。
今回の記事でのゲーム盤のデータ構造の変更点
今回の記事で、ゲーム盤の データ構造 を以下のように変更しました
-
空白のマス を表す文字を半角の空白から、半角の
.
(ピリオド)に変更した -
〇 のマス を表す文字を全角の 〇 から、半角の小文字の
o
(オー)に変更した -
× のマス を表す文字を全角の × から、半角の小文字の
x
(エックス)に変更した
変更後の 〇×ゲームのデータ構造 は以下のようになります。なお、変更した部分は 6 番目の文字列型のデータの部分 のみです。
- 〇×ゲームのゲーム盤のマスの座標を、左右方向を x 座標、上下方向を y 座標とする 2 次元の座標 で表現する
- x 座標はゲーム盤の左端の列を 0 とし、右に 1 つ列がずれるたびに x 座標が 1 増える
- y 座標はゲーム盤の上端の行を 0 とし、下に 1 つ行がずれるたびに y 座標が 1 増える
- ゲーム盤の各マスを表すデータを 2 次元配列 を表す list で表現する
- 2 次元配列の list の 1 つ目のインデックスを x 座標、2 つ目のインデックスを y 座標に対応させる
- 空白、〇、× が配置されたマスを表すデータを、それぞれ半角の
" "
、"o"
(小文字のオー)、"x"
(小文字のエックス) という 文字列型のデータ で表現する - ゲーム盤の各マスを表す list の要素に、そのマスに配置されたマークに対応する文字列を代入する
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
以下のリンクは、今回の記事で更新した marubatsu_new.py です。
次回の記事
更新履歴
更新日時 | 更新内容 |
---|---|
2023/10/14 | デフォルト引数に関する公式の Python のドキュメントへのリンクを追加しました |