目次と前回の記事
実装の進捗状況と前回までのおさらい
〇×ゲームの仕様と進捗状況
正方形で区切られた 3 x 3 の 2 次元のゲーム盤上でゲームを行う
ゲーム開始時には、ゲーム盤の全てのマスは空になっている
2 人のプレイヤーが遊ぶゲームであり、一人は 〇 を、もう一人は × のマークを受け持つ
- 2 人のプレイヤーは、交互に空いている好きなマスに自分のマークを 1 つ置く
- 先手は 〇 のプレイヤーである
- プレイヤーがマークを置いた結果、縦、横、斜めのいずれかの一直線の 3 マスに同じマークが並んだ場合、そのマークのプレイヤーの勝利とし、ゲームが終了する
- すべてのマスが埋まった時にゲームの決着がついていない場合は引き分けとする
仕様の進捗状況は、以下のように表記します。
- 実装が完了した部分を
背景が灰色の長方形
で記述する - 実装の一部が完了した部分を、太字 で記述する
前回までのおさらい
「その 13」の記事では、「ゲーム盤の (x, y) のマスにマークを配置する」関数と、「ゲーム盤を初期化する」関数を定義し、それらの関数が正しく動作するかを確認した結果、バグがある事が判明しました。
「その 14」の記事ではバグが起きる原因を理解するために 必要な知識 として、Python の 名前空間 と スコープ の仕組みについて説明しました。
前回の記事では、名前空間とスコープに関する処理のまとめと、名前解決を 見分けやすい プログラムの 記述方法 について説明しました。
今回の記事では、バグの原因と修正方法、関数の副作用、名前空間が必要になる理由について説明します。
「その 13」のプログラムのバグ
下記に、「その 13」の記事の最後で紹介した、バグのあるプログラムを再掲します。下記のプログラムは、10 行目で initialize_board()
を呼び出して、board
に 初期化されたゲーム盤を代入 しているにも関わらず、11 行目で place_mark(0, 1, "〇")
を呼び出して、(0, 1) のマスに 〇 を配置しようとすると、「( 0 , 1 ) のマスにはマークが配置済です」というメッセージが表示されるというバグがあります。
このことから、initialize_board()
の 関数呼び出しの処理 に、バグ がある可能性が高そうです。
1 def place_mark(x, y, mark):
2 if board[x][y] == " ":
3 board[x][y] = mark
4 else:
5 print("(", x, ",", y, ") のマスにはマークが配置済です")
6
7 def initialize_board():
8 board = [[" "] * 3 for x in range(3)]
9
10 initialize_board() # ゲーム盤の初期化処理を行う関数を呼び出す
11 place_mark(0, 1, "〇") # (0, 1) のマスに 〇 を配置する
12 print(board)
13 place_mark(0, 1, "×") # (0, 1) のマスに × を配置する
14 print(board)
行番号のないプログラム
def place_mark(x, y, mark):
if board[x][y] == " ":
board[x][y] = mark
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
def initialize_board():
board = [[" "] * 3 for x in range(3)]
initialize_board() # ゲーム盤の初期化処理を行う関数を呼び出す
place_mark(0, 1, "〇") # (0, 1) のマスに 〇 を配置する
print(board)
place_mark(0, 1, "×") # (0, 1) のマスに × を配置する
print(board)
実行結果
( 0 , 1 ) のマスにはマークが配置済です
[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
( 0 , 1 ) のマスにはマークが配置済です
[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
実は、このプログラムを 新しい marubatsu.ipynb に 記述して実行 するか、JupyterLab の上部の「再起動」ボタンを クリックしてから実行 すると、上記のような実行結果にはならず、下記の実行結果のように エラーが発生 します。先に、下記のエラーについて説明します。
実行結果
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
c:\Users\ys\ai\marubatsu\016\marubatsu.ipynb セル 1 line 1
8 board = [[" "] * 3 for x in range(3)]
10 initialize_board() # ゲーム盤の初期化処理を行う関数を呼び出す
---> 11 place_mark(0, 1, "〇") # (0, 1) のマスに 〇 を配置する
12 print(board)
13 place_mark(0, 1, "×") # (0, 1) のマスに × を配置する
c:\Users\ys\ai\marubatsu\016\marubatsu.ipynb セル 1 line 2
1 def place_mark(x, y, mark):
----> 2 if board[x][y] == " ":
3 board[x][y] = mark
4 else:
NameError: name 'board' is not defined
上記の実行結果には、上下に 2 つのプログラムが表示されていますが、これは、上の ---->
が表示された 11 行目の place_mark(0, 1, "〇")
の 関数呼び出しによって、下に記述されている place_mark
の関数が呼び出され、その関数のブロックの ---->
が表示されている、プログラムの 2 行目 の if board[x][y] == " ":
で、「NameError: name 'board' is not defined」 という エラーが発生している という意味を表します。
同じ関数 に対する 関数呼び出し の処理が、プログラムの 様々な場所に記述 されることが 良くあります。そのため、関数呼び出しの中 でエラーが発生した場合は、その関数呼び出しがプログラムの どこから呼び出されたか の情報が 重要になる場合が多い ので、上記のように、関数呼び出しが行われた行も併せて エラーメッセージが表示されます。
このエラーメッセージは、board
という 名前(name)が 定義されていない(is not defined)という意味の NameError (名前に関する)エラーで、board
という 名前 が、プログラムの 2 行目をスコープとする どの名前空間にも存在しない ことが原因です。
下記の表は、このプログラムの処理を順を追って説明したものです。
行数 | 行われる処理 |
---|---|
10、7 |
initialize_board() によって、関数呼び出しが行われる。実引数と仮引数は存在しないので仮引数に実引数を代入する処理は行われない |
8 |
board に初期化されたゲーム盤が代入される。この board は initialize_board のローカル変数 なので、この 関数の中でのみ 利用できる。関数呼び出しの処理が終了し、ローカル名前空間が削除され、次の 11 行目に処理が移る |
11、1 |
place_mark(0, 1, "〇") によって、関数呼び出しが行われる。仮引数に実引数を代入する処理が行われるが、エラーの原因には関係がないので説明は省略する |
2 |
if board[x][y] == " ": の処理で、board という名前が記述されているので、名前解決 を行う必要がある。place_mark の ローカル名前空間 には board という名前は 存在しない。また、グローバル名前空間にも、組み込み名前空間にも board という 名前は存在しない。そのため、NameError という エラーが発生 する。なお、8 行目の board を管理していた initialize_board のローカル名前空間は、initialize_board の関数呼び出しが終了した時点で 削除されいる ので 利用することはできない
|
名前空間についての正しい知識がない人は、initialize_board
の中で、グローバル変数の board
に初期化されたゲーム盤を代入しているように 勘違いする かもしれませんが、実際には、8 行目では initialize_board
の ブロックの中でしか利用できない、ローカル変数の board
に初期化されたゲーム盤を代入しています。そのため、2 行目の if board[x][y] == " ":
をスコープとする どの名前空間にも、board
という名前が 登録されていない 事がエラーの原因です。
「その 13」の記事で NameError が発生しない理由
「その 13」の記事でこのプログラムを実行した際に、NameError が発生しなかった理由は、このプログラムを 実行する前 に、「その 13」の記事の中ほどで 下記のプログラムを実行していた からです。
board = [[" "] * 3 for x in range(3)]
if board[0][1] == " ":
board[0][1] = "〇"
else:
print("(0, 1) のマスにはマークが配置済です")
上記のプログラムを実行した結果、グローバル変数の board
の値は、[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
というデータになります。その状態で、先程のプログラムを実行すると、2 行目の if board[x][y] == " ":
をスコープとするグローバル名前空間に、上記のデータを管理するオブジェクトに対応づけられた board
が登録されている ので、エラーは発生しません。ただし、board[0][1]
の値は "〇"
なので、この if 文の 条件式の計算結果は False
になります。そのため、「(0, 1) のマスにはマークが配置済です」というメッセージが表示されます。
プログラムの修正方法
先程のプログラムは、initialize_board
のブロックの中で、ローカル変数である board
に対して 初期化されたゲーム盤を 代入する 処理を行っていた点が バグの原因 でした。
今回の記事では、この問題を解決する 2 種類の方法を紹介します。
初期化されたゲーム盤のデータを関数の返り値として返す方法
一つ目は、initilaize_board
で行う処理を、下記のプログラムの 8 行目のように、初期化されたゲーム盤のデータを 関数の返り値として返す ように修正し、10 行目のように、 関数のブロックの外 で グローバル変数 board
に関数の 返り値を代入する ように修正するという方法です。
下記のプログラムを、JupyterLab の 「再起動」ボタンを押してから実行 しても、エラーは発生しません。また、実行結果から、11 行目の place_mark
によってマークが配置済であるという メッセージが表示されなくなった ことを確認することが出来ます。
1 def place_mark(x, y, mark):
2 if board[x][y] == " ":
3 board[x][y] = mark
4 else:
5 print("(", x, ",", y, ") のマスにはマークが配置済です")
6
7 def initialize_board():
8 return [[" "] * 3 for x in range(3)]
9
10 board = initialize_board() # 初期化されたゲーム盤のデータを返す関数を呼び出す
11 place_mark(0, 1, "〇") # (0, 1) のマスに 〇 を配置する
12 print(board)
13 place_mark(0, 1, "×") # (0, 1) のマスに × を配置する
14 print(board)
行番号のないプログラム
def place_mark(x, y, mark):
if board[x][y] == " ":
board[x][y] = mark
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
def initialize_board():
return [[" "] * 3 for x in range(3)]
board = initialize_board() # 初期化されたゲーム盤のデータを返す関数を呼び出す
place_mark(0, 1, "〇") # (0, 1) のマスに 〇 を配置する
print(board)
place_mark(0, 1, "×") # (0, 1) のマスに × を配置する
print(board)
修正箇所
def place_mark(x, y, mark):
if board[x][y] == " ":
board[x][y] = mark
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
def initialize_board():
- board = [[" "] * 3 for x in range(3)]
+ return [[" "] * 3 for x in range(3)]
- initialize_board() # 初期化されたゲーム盤のデータを返す関数を呼び出す
+ board = initialize_board() # 初期化されたゲーム盤のデータを返す関数を呼び出す
place_mark(0, 1, "〇") # (0, 1) のマスに 〇 を配置する
print(board)
place_mark(0, 1, "×") # (0, 1) のマスに × を配置する
print(board)
実行結果
[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
( 0 , 1 ) のマスにはマークが配置済です
[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
上記の、関数の返り値をグローバル変数に代入する方法 については、前回の記事の最後 で紹介しましたので、忘れた方は前回の記事を復習して下さい。
上記のプログラムに関する補足
上記の修正したプログラムを見た方の中で、initialize_board
を以下のような処理を行うように定義したほうが良いと思った方はいないでしょうか?
-
仮引数
board
で、グローバル変数board
の値を 受け取る -
initialize_board
の ブロックの中 で、ローカル変数board
に対して初期化されたゲーム盤を 代入する -
ローカル変数
board
を関数の 返り値として返す ようにし、グローバル変数board
にその 返り値を代入する
下記はそのように initialize_board
を定義したプログラムです。しかし、実際にこのプログラムを、JupyterLab の 「再起動」ボタンを押してから実行 すると、下記のようなエラーが発生してしまいます。
def initialize_board(board):
board = [[" "] * 3 for x in range(3)]
return board
board = initialize_board(board)
実行結果
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
c:\Users\ys\ai\marubatsu\016\marubatsu.ipynb セル 4 line 5
2 board = [[" "] * 3 for x in range(3)]
3 return board
----> 5 board = initialize_board(board)
NameError: name 'board' is not defined
このエラーは、5 行目 の initialize_board(board)
の 関数呼び出しに原因 があります。この関数呼び出しには、board
という名前 が記述されているので、名前解決を行う必要 がありますが、この時点では board
というグローバル変数に値が代入されていません。そのため、グローバル名前空間 に board
という 名前が登録されていない ため、NameError が発生 します。
上記のプログラムを 実行する前 に、「再起動」ボタンを押した のは、今回の記事では、上記のプログラムを 実行する前に、board = initialize_board()
を 実行している ため、グローバル名前空間に board
という 名前が登録されている ためです。
「再起動」ボタンを押してから上記のプログラムを実行することにより、Python のプログラムが 新しく実行 され、グローバル名前空間 が 空の状態 で新しく 作り直されます。
また、仮にグローバル変数 board
に 値が代入されていたとしても、initialize_board
の 仮引数 に board
は 必要ありません。その理由は、initialize_board
の 2 行目で、ローカル変数である仮引数 board
に 代入する式の中 に、ローカル変数 board
の値が使われていない からです。別の言葉で説明すると、関数が行う処理の中で、関数に入力した 仮引数のデータを使うことがない のであれば、その 入力データ はそもそも 必要がない ということです。
分かりづらいと思いますので、プログラムの具体例を挙げます。下記のプログラムの 1、2 行目の 1
という数字を表示する print_one
という関数に、仮引数 a
が必要だと思いますか?
この関数では、仮引数 a
の値 は、関数の ブロック内で一度も使われていません。従って、関数呼び出しの際に、どのような値を実引数に記述 したとしても、この関数の処理に 影響は与えません。つまり、仮引数 a
はこの関数には 不要である ということです。
def print_one(a):
print(1)
下記のプログラムは、2 行目で仮引数 a
に 1
を代入し、3 行目で a
の値を表示しているので上記のプログラムと同様に、必ず 1
が表示 されます。2 行目で行われる処理は、ローカル変数である仮引数に a
にどのような値が代入されていたとしても、その値を利用することなく a
に 1
を 代入して上書きする 処理を行っています。従って、関数呼び出しの際に、どのような値を実引数に記述 したとしても、この関数の処理に 影響は与えません。つまり、仮引数 a
はこの関数には 不要である ということです。
def print_one(a):
a = 1
print(a)
上記の print_one
と先ほどの initialize_board
は、どちらも 仮引数 に、仮引数の値を使わない式を代入 しています。従って、initialize_board
の 仮引数は不要です。
関数の返り値のローカル変数への代入
initialize_board
を下記のように、初期化されたゲーム盤のデータを一旦 ローカル変数 board
に代入 してから、その board
を返り値として返す ように定義する事もできます。
def initialize_board():
board = [[" "] * 3 for x in range(3)]
return board
上記のプログラムは、先ほどの補足で紹介したバグのあるプログラムに 似ているように見える かもしれませんが、仮引数を持たない 点が異なりまります。
主に、以下のような場合に関数の返り値を ローカル変数 に一旦 代入 してから、return ローカル変数
を記述して返り値を返します。
- 関数の返り値を計算するために、複数の文を記述 する必要がある場合
- 分かりやすい名前 のローカル変数に関数の返り値の値を代入することによって、関数がどのようなデータを返り値として返すかを わかりやすく示したい 場合
上記のプログラムは、返り値を 1 行の式で計算できるため、上記の 1 つ目の条件は満たしませんが、board
という名前のローカル変数に返り値の値を代入することで、この関数がゲーム盤(board)のデータを返していることが return 文を見ただけで分かるよう になります。
返り値をローカル変数に代入する場合も、そうでない場合も、どちらも間違ってはいない ので、自分が 分かりやすいと思った方法で プログラムを記述すると良いでしょう。
本記事の initialize_board
の定義は、返り値をローカル変数に代入しない方法を採用します。
global を使う方法
もう 1 つの解決方法は、関数の ブロックの中 で グローバル変数に対して直接代入処理を行う というものです。関数のブロックの中 に下記のように記述することで、その関数のブロック内では、global
の後に記述した変数 を、グローバル変数とみなして処理が行われる ようになります1。
global 変数名
関数のブロックの中に global 変数名
を記述すると、その名前はその関数の ブロック内 では、ローカル変数として 使用することができなくなります。例えば、global 変数名
で指定した変数名を、ローカル変数である 仮引数の名前に使用 することは できません。
下記のプログラムはその具体例です。
def a(x):
global x
実行結果
Cell In[5], line 2
global x
^
SyntaxError: name 'x' is parameter and global
上記のエラーメッセージは、以下のような意味を持ちます。
-
SyntaxError
構文(文法のこと)(syntax)に関するエラー -
invalid syntax
x
という名前(name)は、(ローカルな)仮引数(parameter)であり、なおかつ(and)グローバル(global)でもある。これは、x
が 同時に ローカルとグローバルな 名前 であることが 矛盾している ということを表すメッセージです
下記が global
を使ってバグを修正したプログラムです。修正前のプログラムとの違いは、8 行目に global board
が追加 されている点のみです。その結果、9 行目の board
は グローバル変数の board
に初期化されたゲーム盤を代入する処理になるので、この関数に return 文を記述するように 修正する必要も、11 行目を board = initialize_board()
のように 修正する必要もありません。
下記の実行結果は、JupyterLab で 「再起動」ボタンをクリックしてから プログラムを実行した場合のものです。実行結果から、上記のプログラムで エラーが発生しなくなった ことを確認することができます。また、12 行目の place_mark
によってマークが配置済であるという メッセージが表示されなくなった ことを確認することが出来ます。
1 def place_mark(x, y, mark):
2 if board[x][y] == " ":
3 board[x][y] = mark
4 else:
5 print("(", x, ",", y, ") のマスにはマークが配置済です")
6
7 def initialize_board():
8 global board
9 board = [[" "] * 3 for x in range(3)]
10
11 initialize_board() # ゲーム盤の初期化処理を行う関数を呼び出す
12 place_mark(0, 1, "〇") # (0, 1) のマスに 〇 を配置する
13 print(board)
14 place_mark(0, 1, "×") # (0, 1) のマスに × を配置する
15 print(board)
行番号のないプログラム
def place_mark(x, y, mark):
if board[x][y] == " ":
board[x][y] = mark
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
def initialize_board():
global board
board = [[" "] * 3 for x in range(3)]
initialize_board() # ゲーム盤の初期化処理を行う関数を呼び出す
place_mark(0, 1, "〇") # (0, 1) のマスに 〇 を配置する
print(board)
place_mark(0, 1, "×") # (0, 1) のマスに × を配置する
print(board)
修正箇所
def place_mark(x, y, mark):
if board[x][y] == " ":
board[x][y] = mark
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
def initialize_board():
+ global board
board = [[" "] * 3 for x in range(3)]
initialize_board() # ゲーム盤の初期化処理を行う関数を呼び出す
place_mark(0, 1, "〇") # (0, 1) のマスに 〇 を配置する
print(board)
place_mark(0, 1, "×") # (0, 1) のマスに × を配置する
print(board)
実行結果
[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
( 0 , 1 ) のマスにはマークが配置済です
[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
global を使用する際の注意点
global
を使用すると、関数のブロックの中 に、ローカル変数とグローバル変数が 混在するようになる ので、前回の記事で説明した 名前解決を見分けやすい プログラムの記述方法 ではなくなる、関数で行われる処理が わかりづらくなる などの 欠点 が発生します。そのため、global
の使用 は一般的には 推奨されません。
ただし、実際には global
を 使ったほうが良い 場合や、global
を 使わざるを得ない 場合があるので説明しました。本記事では、必要がない限りは、global
を使わない ことにし、使う必要がある場合はその理由を説明した上で使うことにします。
place_mark
の修正
place_mark
の ブロックの中 の board
は、グローバル変数 なので、place_mark
のブロックの中にローカル変数とグローバル変数が 混在しています。そこで、下記のプログラムのように、place_mark
にゲーム盤のデータを代入する 仮引数 board
を追加 することで、関数のブロックの中に ローカル変数のみが存在 するように修正することにします。
1 def place_mark(board, x, y, mark):
2 if board[x][y] == " ":
3 board[x][y] = mark
4 else:
5 print("(", x, ",", y, ") のマスにはマークが配置済です")
6
7 def initialize_board():
8 return [[" "] * 3 for x in range(3)]
9
10 board = initialize_board() # 初期化されたゲーム盤のデータを返す関数を呼び出す
11 place_mark(board, 0, 1, "〇") # (0, 1) のマスに 〇 を配置する
12 print(board)
13 place_mark(board, 0, 1, "×") # (0, 1) のマスに × を配置する
14 print(board)
行番号のないプログラム
def place_mark(board, x, y, mark):
if board[x][y] == " ":
board[x][y] = mark
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
def initialize_board():
return [[" "] * 3 for x in range(3)]
board = initialize_board() # 初期化されたゲーム盤のデータを返す関数を呼び出す
place_mark(board, 0, 1, "〇") # (0, 1) のマスに 〇 を配置する
print(board)
place_mark(board, 0, 1, "×") # (0, 1) のマスに × を配置する
print(board)
修正箇所
- def place_mark(x, y, mark):
+ def place_mark(board, x, y, mark):
if board[x][y] == " ":
board[x][y] = mark
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
def initialize_board():
return [[" "] * 3 for x in range(3)]
board = initialize_board() # 初期化されたゲーム盤のデータを返す関数を呼び出す
- place_mark(0, 1, "〇") # (0, 1) のマスに 〇 を配置する
+ place_mark(board, 0, 1, "〇") # (0, 1) のマスに 〇 を配置する
print(board)
- place_mark(0, 1, "×") # (0, 1) のマスに × を配置する
+ place_mark(board, 0, 1, "×") # (0, 1) のマスに × を配置する
print(board)
実行結果
[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
( 0 , 1 ) のマスにはマークが配置済です
[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
上記のプログラムの修正箇所と修正内容は以下の通りです
行数 | 修正内容 |
---|---|
1 | 仮引数の先頭に board を追加 することで、ブロック内の board がローカル変数 になるようにする |
11、13 |
place_mark の関数呼び出しの先頭の 実引数 に グローバル変数 board を記述することで、仮引数 board に 初期化されたゲーム盤を代入 するようにする |
実行結果から、上記のプログラムが正しく動作していることを確認することが出来ます。
修正後の place_mark
の利点
修正後の place_mark
には 大きな利点 があります。修正前のプログラムは、ゲーム盤のデータを board
という名前 の 1 つの グローバル変数 でしか扱うことが出来ません が、修正後のプログラムでは、任意の数の ゲーム盤のデータを、任意の名前の変数 に代入することが出来ます。
下記のプログラムは、board1
と board2
という変数に、それぞれ 別の 初期化されたゲーム盤を 代入 し、board1
には (0, 1) のマスに 〇 を、board2
には (1, 0) のマスに ×
を配置しています。実行結果からわかるように、board1
と board2
は 別々のゲーム盤を表している ことがわかります。
def place_mark(board, x, y, mark):
if board[x][y] == " ":
board[x][y] = mark
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
def initialize_board():
return [[" "] * 3 for x in range(3)]
board1 = initialize_board() # board1 に初期化されたゲーム盤を代入する
place_mark(board1, 0, 1, "〇") # board1 の (0, 1) のマスに 〇 を配置する
print(board1)
board2 = initialize_board() # board2 に初期化されたゲーム盤を代入する
place_mark(board2, 1, 0, "×") # board2 の (1, 0) のマスに × を配置する
print(board2)
実行結果
[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
[[' ', ' ', ' '], ['×', ' ', ' '], [' ', ' ', ' ']]
place_mark
の修正に関する補足
place_mark
が、ローカル変数 board
を関数の 返り値として返していない 点が 気になる人 はいないでしょうか?そのような人は、place_mark
を下記のプログラムのように修正する必要があると 思うかもしれません。
下記のプログラムは、6 行目に return board
を追加 し、12、14 行目で グローバル変数 board
に place_mark
の 返り値を代入 するように修正しています。
1 def place_mark(board, x, y, mark):
2 if board[x][y] == " ":
3 board[x][y] = mark
4 else:
5 print("(", x, ",", y, ") のマスにはマークが配置済です")
6 return board
7
8 def initialize_board():
9 return [[" "] * 3 for x in range(3)]
10
11 board = initialize_board() # 初期化されたゲーム盤のデータを返す関数を呼び出す
12 board = place_mark(board, 0, 1, "〇") # (0, 1) のマスに 〇 を配置する
13 print(board)
14 board = place_mark(board, 0, 1, "×") # (0, 1) のマスに × を配置する
15 print(board)
行番号のないプログラム
def place_mark(board, x, y, mark):
if board[x][y] == " ":
board[x][y] = mark
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
return board
def initialize_board():
return [[" "] * 3 for x in range(3)]
board = initialize_board() # 初期化されたゲーム盤のデータを返す関数を呼び出す
board = place_mark(board, 0, 1, "〇") # (0, 1) のマスに 〇 を配置する
print(board)
board = place_mark(board, 0, 1, "×") # (0, 1) のマスに × を配置する
print(board)
修正箇所
def place_mark(board, x, y, mark):
if board[x][y] == " ":
board[x][y] = mark
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
+ return board
def initialize_board():
return [[" "] * 3 for x in range(3)]
board = initialize_board() # 初期化されたゲーム盤のデータを返す関数を呼び出す
- place_mark(board, 0, 1, "〇") # (0, 1) のマスに 〇 を配置する
+ board = place_mark(board, 0, 1, "〇") # (0, 1) のマスに 〇 を配置する
print(board)
- place_mark(board, 0, 1, "×") # (0, 1) のマスに × を配置する
+ board = place_mark(board, 0, 1, "×") # (0, 1) のマスに × を配置する
print(board)
実行結果
[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
( 0 , 1 ) のマスにはマークが配置済です
[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
実行結果から確認できるように、このような修正を行ってもプログラムは 正しく動作します が、実際にはこのような 修正を行う必要はありません。その理由は、place_mark
のブロックの中の処理では、board
に 値を 代入 するの ではなく、board
の要素 に 値を 代入している からです。
place_mark(board, 0, 1, "〇")
の関数呼び出しによって、グローバル変数 board
とローカル変数 board
は 同じデータを管理 する オブジェクトを共有する ようになります。3 行目の board[x][y] = mark
によって、2 次元配列を表す list が代入されている board
の (x, y) のマスを表す 要素に mark
が代入されますが、その際に、グローバル変数 board
とローカル変数 board
の 共有は解除されません。従って、ローカル変数 board
の要素を変更すると、同時にグローバル変数 board
の要素も変更されます。そのため、place_mark
の場合は、ローカル変数 board
を 返り値として返す必要はありません。
なお、initialize_board
の場合は、place_mark
のように 既に存在する ゲーム盤のデータの 一部を変更 するの ではなく 、新しいゲーム盤のデータを 無から作成している ので、返り値としてゲーム盤のデータを返す必要があります。
関数の ブロック内の処理 によって、グローバル変数の値が変更される ことがピンとこない人がいるかもしれませんので、次はその点について説明します。
関数の副作用
関数呼び出しの処理によって、関数の ブロックの中 で、関数の外の グローバル変数の値が変化 することを、関数の 副作用 と呼びます。
前回の記事で、「関数の ブロックの中の変数 に、仮引数 か、ブロックの中で値を代入した変数のみ を記述する」という条件を守った場合、関数の内外で ローカル変数とグローバル変数 を 分離する ことができるという説明をしました。ただし、今回の記事で global
を使って関数のブロックの中でグローバル変数に値を代入する方法を説明しましたので、上記の条件は global
を使わない場合 の条件です。
先程修正した initialize_board
は上記の条件を 満たしており、initialize_board
の関数呼び出しによって、どのグローバル変数の値も 変化しない ので、関数の 副作用は発生しません。
一方、先程修正した place_mark
は上記の条件を 満たしていますが、place_mark
の関数呼び出しによって、グローバル変数 board
の値が 変化する ので、関数の 副作用が発生しています。
このことから、上記の条件を満たしても、関数の副作用が 発生する場合 と 発生しない場合 がある事がわかります。
そこで、「関数の ブロックの中の変数 は、仮引数 か、ブロックの中で値を代入した変数のみ を記述する」という 条件が満たされた場合 に、関数の 副作用が発生する条件 について説明します。
global を使う場合
global
を使うと、関数のブロックの中でグローバル変数に値を直接代入できるので、global
が記述された関数内で、グローバル変数に値を代入した場合は、関数の副作用が発生します。
以後の説明は、関数のブロックの中で、global
を記述しない 場合の説明です。
関数に仮引数が存在しない場合
関数に仮引数が存在しない場合は、関数のブロックの外の グローバル変数の値 は、関数の ブロック内で行われる処理 に一切 影響を及ぼしません。また、関数のブロック内ではグローバル変数に値を代入することはできないので、関数の副作用は発生しません。
関数に仮引数が存在する場合
関数に仮引数が存在する場合は、関数の 仮引数に、関数呼び出しの 実引数の値が代入 され、関数のブロック内で 仮引数の値を使って 処理が行われます。
仮引数の値の一部を変更しない場合
副作用は、複数の変数が共有するデータの 一部を変更する際に発生する ものなので、仮引数が存在する場合でも、仮引数の値を利用するだけ で、仮引数の 値の一部を更しない 場合は 副作用は発生しません_。
ここでいう、仮引数の値の一部の変更 とは、仮引数に直接値を代入するの ではなく、list の要素に値を代入するような、仮引数が 参照するオブジェクトを変更せず に、仮引数に代入された 値の一部を変更 するような処理の事を指します。
実引数が変数のみの式の場合
実引数が a
のような 変数のみの式 の場合、過去の記事で説明したように、仮引数に実引数を代入する処理によって、仮引数と実引数が 同じオブジェクトを共有 するようになります。
仮引数に代入されたデータが、疑似的な複製が行われるデータの場合
「疑似的な複製 が行われるデータ」は、本記事が「その 11」の記事で 独自に定義した用語 で、「データを構成する、全てのオブジェクト が管理する データ が イミュータブル2 なデータ」のことです。良く使われる疑似的な複製が行われるデータ型には、数値型、文字列型、論理型、None 型 のデータが挙げられます。
疑似的な複製が行われるデータが代入された変数は、その性質から、別のデータ で変数の値を 上書き する 代入以外 の処理で 値を変更することはできません。また、仮引数に値を代入すると、仮引数と実引数の オブジェクトの共有が解除 されます。従って、仮引数に対してどのような処理を行っても、実引数に記述したグローバル変数 の値が 変化することはありません。従って、この場合は 関数の副作用は発生しません。
分かりにくいと思いますので、具体例を挙げます。下記のプログラムは過去の記事で紹介した assign_one
と同じプログラムです。5 行目で、assign_one
の関数呼び出しの 実引数 に グローバル変数 a
を記述 しているため、グローバル変数 a
と assign_one
の ローカル変数 である仮引数 a
が同じオブジェクトを 共有 します。
ローカル変数 a
に代入された 0
は、データを構成する、全て のオブジェクトが管理する データ が イミュータブルなデータ なので、新しいデータを 代入する以外の方法 で、変数の 値を変更することはできません。また、2 行目のような 代入処理を行う と、グローバル変数 a
とローカル変数 a
の 共有が解除 されてしまいます。従って、ローカル変数 a
に対してどのような処理を行っても、グローバル変数 a
の値 を 変化させることはできません。この説明は、過去の記事の プログラム A の説明と同じなので忘れた方は「その 11」前後の記事を復習して下さい。
1 def assign_one(a):
2 a = 1
3
4 a = 0
5 assign_one(a)
6 print(a)
行番号のないプログラム
def assign_one(a):
a = 1
a = 0
assign_one(a)
print(a)
実行結果
0
仮引数に代入されたデータが、疑似的な複製が行われないデータの場合
「疑似的な複製が 行われない データ」とは、「データを構成する、1 つ以上のオブジェクト が管理するデータが ミュータブル3 なデータ」のことです。代表的なデータ型には、list や、本記事ではまだ説明していない dict などがあります。
疑似的な複製が行われないデータが代入された変数は、その性質から、データを構成する ミュータブルなデータ の値を 上書き して 変更する ことが出来ます。従って、実引数に記述したグローバル変数 の値が 変化することがある ため、この場合は、関数の副作用は発生する可能性 があります。
例えば、仮引数 に対して 何の処理も行わない ような関数の場合は、副作用は発生しないので、上記では「発生する可能性がある」という表現になっています。
分かりにくいと思いますので、具体例を挙げます。下記のプログラムは先ほど修正した place_mark
を再掲したものです。11 行目で、place_mark
の関数呼び出しの 最初の実引数 に、グローバル変数 board
を記述しているので、グローバル変数 board
と place_mark
のローカル変数 board
が 同じオブジェクトを共有 します。
ローカル変数 board
に代入された list はミュータブルなデータなので、3 行目のように、その要素に値を代入することが出来ます。その際にグローバル変数 a
とローカル変数 a
の 共有は解除されません。従って、3 行目の処理を行うと、グローバル変数 board
の値 が 変化 してしまいます。この説明は、過去の記事の プログラム B の説明と同じなので忘れた方は「その 11」前後の記事を復習して下さい。
1 def place_mark(board, x, y, mark):
2 if board[x][y] == " ":
3 board[x][y] = mark
4 else:
5 print("(", x, ",", y, ") のマスにはマークが配置済です")
6
7 def initialize_board():
8 return [[" "] * 3 for x in range(3)]
9
10 board = initialize_board() # 初期化されたゲーム盤のデータを返す関数を呼び出す
11 place_mark(board, 0, 1, "〇") # (0, 1) のマスに 〇 を配置する
12 print(board)
13 place_mark(board, 0, 1, "×") # (0, 1) のマスに × を配置する
14 print(board)
行番号のないプログラム
def place_mark(board, x, y, mark):
if board[x][y] == " ":
board[x][y] = mark
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
def initialize_board():
return [[" "] * 3 for x in range(3)]
board = initialize_board() # 初期化されたゲーム盤のデータを返す関数を呼び出す
place_mark(board, 0, 1, "〇") # (0, 1) のマスに 〇 を配置する
print(board)
place_mark(board, 0, 1, "×") # (0, 1) のマスに × を配置する
print(board)
厳密な副作用が起きる範囲
厳密には、仮引数に代入されたデータ を 共有、または 部分的に共有 する グローバル変数 に対して のみ、副作用が発生します。部分的な共有 とは、異なる変数 に代入された 異なるデータ が、一部のデータ を 共有する状態 の事を指します。詳細は「その 11」の記事で説明しているので忘れた方は復習して下さい。
おそらくこの説明で意味が分かる人は少ないと思いますので具体例を挙げます。下記のプログラムは、初期化されたゲーム盤の 各行 を row0
、row1
、row2
という グローバル変数に代入 し、それらを使って グローバル変数 board
に初期化されたゲーム盤を代入しています。
この場合は、board
に代入されたデータを 構成する 3 つのオブジェクト をグローバル変数 row0
、row1
、row2
が参照しているので、board
と row0
などは、データを 部分的に共有 しています。
このような場合に place_mark
で (0, 1) のマスに 〇 を配置すると、下記の実行結果のように、board
だけでなく、board
に代入されたデータを構成するオブジェクトの 1 つを参照する row0
の値も 変更 されてしまいます。
def place_mark(board, x, y, mark):
if board[x][y] == " ":
board[x][y] = mark
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
row0 = [" ", " ", " "]
row1 = [" ", " ", " "]
row2 = [" ", " ", " "]
board = [row0, row1, row2]
place_mark(board, 0, 1, "〇")
print(board)
print(row0)
実行結果
[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
[' ', '〇', ' ']
実引数が変数のみ式でない場合
実引数が a + b
のような、変数のみ式でない 場合も、副作用が発生する条件は、実引数が変数のみ式の場合と 大きく変わることはありません。
実引数の式の 計算結果 が 疑似的な複製が行われるデータ であれば、__副作用は発生しません__し、そうでなければ先ほど 厳密な副作用が起きる範囲 で説明した場合と 同じ範囲 で 副作用が発生する可能性 があります。具体例を挙げて説明します。
先程のプログラムでは、初期化されたゲーム盤のデータを グローバル変数 board
に代入 し、その board
を place_mark
の実引数に記述 していました。
一方、下記のプログラムのように、初期化されたゲーム盤のデータを グローバル変数に代入せず に、[row0, row1, row2]
のように、直接 place_mark
の 実引数に記述 することもできます。下記のプログラムでは、実引数が 変数のみの式ではありません が、実行結果からわかるように、グローバル変数 row0
の値が変化しているので、副作用が発生する ことがわかります。
def place_mark(board, x, y, mark):
if board[x][y] == " ":
board[x][y] = mark
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
row0 = [" ", " ", " "]
row1 = [" ", " ", " "]
row2 = [" ", " ", " "]
place_mark([row0, row1, row2], 0, 1, "〇")
print(row0)
修正箇所
def place_mark(board, x, y, mark):
if board[x][y] == " ":
board[x][y] = mark
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
row0 = [" ", " ", " "]
row1 = [" ", " ", " "]
row2 = [" ", " ", " "]
- board = [row0, row1, row2]
- place_mark(board, 0, 1, "〇")
+ place_mark([row0, row1, row2], 0, 1, "〇")
- print(board)
print(row0)
実行結果
[' ', '〇', ' ']
副作用が起きる範囲の制御
ここまでの説明からわかるように、「関数のブロックの中の変数に、仮引数か、ブロックの中で値を代入した変数のみを記述する」と「global を使用しない」という条件を満たした場合に、関数の副作用は 無秩序に発生する わけでは ありません。具体的には、仮引数 に代入されたデータと 無関係なグローバル変数 に対しては 副作用は発生しません。
このことから、上記の条件を満たした場合は、実引数によって、関数の副作用が発生する可能性のあるグローバル変数を 制御することができる ことがわかります。
関数の副作用のまとめ
下記の 2 つの条件 を満たした場合の関数の副作用をまとめます。
- 関数の ブロックの中の変数 に、仮引数 か、ブロックの中で値を代入した変数のみ を記述する
- global を 使用しない
下記の いずれかの条件 を満たす場合は、関数の 副作用は発生しません。
- 仮引数が存在しない 場合
- 関数のブロックの中で、仮引数に代入された 値の一部を変更しない 場合
- 仮引数に代入された値が、疑似的な複製が行われる データである場合
上記以外の場合は、仮引数に代入されたデータと 関係のある グローバル変数に対して のみ 副作用が発生する可能性がある。ここでいう 関係のある とは、仮引数に代入されたデータ を 共有、または 部分的に共有 する グローバル変数 のことである。
名前空間とスコープが必要になる理由
ここまでの説明で、名前空間とスコープの扱い方がなんとなくわかってきた人が多いと思いますが、この仕組みは複雑なだけで、メリットを感じられない 人が多いのではないかと思います。そこで、今回の記事では最後に名前空間とスコープが 必要になる理由 について説明します。
異なる関数で同じ名前のローカル変数を使う
プログラムでは、異なる関数 で 同じような意味 を持つデータを代入する ローカル変数を扱う 場合が良くあります。例えば、place_mark
という関数は、ゲーム盤 と x 座標 と y 座標 のデータを扱うので、ローカル変数 である 仮引数の名前 に下記のように board
と x
と y
という名前を付けました。
def place_mark(board, x, y, mark):
if board[x][y] == " ":
board[x][y] = mark
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
次に、ゲーム盤の (x, y) のマスのマークを取り除く(remove)、remove_mark
という関数を定義する事を考えてみて下さい。remove_mark
も ゲーム盤 と x 座標 と y 座標 のデータを扱うので、下記のプログラムのように、仮引数の名前 に board
と x
と y
という名前を付けるのが自然でしょう。
マークを取り除くということは、(x, y) のマスを表す要素に 半角の空白文字を代入 すればよいので、remove_mark
は下記のプログラムのように定義する事ができます。
def remove_mark(board, x, y):
board[x][y] = " "
(x, y) のマスが空白の場合にこの処理を実行すると、空白のマスを空白にするという処理が行われるので、問題は発生しません。そのため place_mark
と異なり、remove_mark
では if 文による空白のマスであるかどうかのチェックを 行う必要はありません。
place_mark
と remove_mark
はどちらも ゲーム盤 と x 座標 と y 座標 を表すデータを代入する board
と x
と y
という 同じ名前の仮引数 を持ちますが、それらは 名前は同じ ですが 別の変数 です。
異なる変数に 対して 必ず異なる名前 を付けなければ ならない 場合は、place_mark
と remove_mark
を 同じプログラムで同時に定義 する際に、下記のプログラムのようにそれぞれの関数の仮引数の 名前をすべて異なる名前 にする 必要 があります。
def place_mark(board, x, y, mark):
if board[x][y] == " ":
board[x][y] = mark
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
def remove_mark(board2, x2, y2):
board2[x2][y2] = " "
ゲーム盤 と x 座標と y 座標 を表すデータを 入力 とする関数を さらに定義 する場合は、その関数の仮引数の名前を、上記の board
、x
、y
、board2
、y2
、y2
以外の名前にする必要 があります。さすがに、それは大変すぎますし、そのような関数が増えれば増えるほど、既に定義した関数の仮引数に使った名前と 重複しない 変数名を考えることは 困難になります。
名前空間の仕組み があれば、関数呼び出しのたびに 新しいローカル名前空間が作成 されます。同じ名前 のローカル変数であっても、その変数名を管理する 名前空間が異なれば、異なるオブジェクトに対応づけられる ので、異なる関数 の中で 同じ名前のローカル変数 を使っても、それらを 異なる変数 として 区別できる ようになります。
そのため、ゲーム盤 と x 座標 と y 座標 を表すデータを仮引数とする関数は board
と x
と y
という 共通の名前 の仮引数を使って関数の定義を行うことが出来るようになります。従って、下記のよう、同じプログラムの中に、同じ名前 の仮引数を持つ、place_mark
と remove_mark
を 同時に定義 して利用することができます。
def place_mark(board, x, y, mark):
if board[x][y] == " ":
board[x][y] = mark
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
def remove_mark(board, x, y):
board[x][y] = " "
なお、上記の例では異なる関数で、同じ名前の仮引数 を記述する例を挙げましたが、異なる関数で 仮引数以外 の 同じ名前のローカル変数 を 記述 することもできます。
名前空間の仕組みがあることで、他の関数で使われたローカル変数の名前 を 気にすることなく、関数の定義を記述 することが出来るようになる。
グローバル変数とローカル変数で同じ名前を使う
上記の場合は、異なる関数 で 同じ名前 の ローカル変数 を使うという例でしたが、グローバル変数とローカル変数 に 同じ名前を使える と便利な場合があります。
例えば、place_mark
を使って、全てのマスに 〇 を配置するプログラムを作成することを考えてみて下さい。そのような処理は、「その 6」の記事で紹介した、for 文を入れ子にした繰り返し処理 を使って、下記のようなプログラムで記述することが出来ます。
def place_mark(board, x, y, mark):
if board[x][y] == " ":
board[x][y] = mark
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
def initialize_board():
return [[" "] * 3 for x in range(3)]
# 初期化されたゲーム盤のデータを返す関数を呼び出す
board = initialize_board()
# 各列の処理を繰り返し処理で行う
for x in range(3):
# x 列の、各行の処理を繰り返し処理で行う
for y in range(3):
# (x, y) のマスに "〇" を配置する
place_mark(board, x, y, "〇")
print(board)
実行結果
[['〇', '〇', '〇'], ['〇', '〇', '〇'], ['〇', '〇', '〇']]
上記のプログラムでは、2 つの 入れ子になった for 文で、列を表す x 座標と、行を表す y 座標の値を、for 文による繰り返し処理のたびに range(3)
から取り出して利用していますが、その際に取り出した値を x
と y
という名前の グローバル変数に代入 しています。また、ゲーム盤のデータは、board
という名前の グローバル変数に代入 しています。
異なる変数に 対して 必ず異なる名前 を付けなければ ならない場合 は、この 3 つのグローバル変数 x
、y
、board
と、place_mark
の仮引数である ローカル変数 x
、y
、board
の名前をそれぞれ 異なる名前にする必要 がありますが、それは 大変 だと思いませんか?
名前空間の仕組みがあれば、グローバル変数と、ローカル変数は、同じ名前であっても異なる変数 として扱われます。従って、上記のプログラムのように、同じ名前 のグローバル変数とローカル変数を記述しても、それらは 別の変数として区別される ので、プログラムは 正しく動作 します。
名前空間の仕組みがあることで、以下のようなメリットが得られる。
-
他の関数 のブロックの中で使われた ローカル変数の名前 を 気にすることなく、関数のブロックの中 で ローカル変数の名前を付ける ことが出来るようになる。
-
関数の外で使われた グローバル変数の名前 を 気にすることなく、関数のブロックの中 で ローカル変数の名前を付ける ことが出来るようになる。
-
すべての関数 のブロックの中で使われた ローカル変数の名前 を 気にすることなく、関数のブロックの外 で グローバル変数の名前を付ける ことが出来るようになる。
異なるモジュールで同じ名前を使えるようにする
名前空間が必要となる他の状況として、ファイルに保存 された Python のプログラム である モジュール を、自分のプログラムに インポートして利用 したい場合があります。モジュールには、様々な変数や関数が記述 されていますが、それらの変数名や関数名と 同じ名前 の変数や関数を自分のプログラムで 使えないと、プログラムを記述することが 非常に困難 になってしまいます。
名前空間の仕組みがあれば、他のモジュールをインポートして利用する際に、インポートするモジュール内で どのような変数名や関数名が記述されているか について、気にすることなく 自分のプログラムを記述することが出来るようになります。
モジュールのインポートの方法と、インポートしたモジュール内の名前の記述方法については、必要になった時点で説明します。
名前空間の仕組みがあることで、他のモジュールで使われた名前 を 気にすることなく、プログラムを記述 することが出来るようになる。
まとめ
名前空間の仕組みによって、異なる名前空間 で使われている 名前を気にすることなく、名前を記述 できるようになるというメリットが得られる。
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
おそらく、VSCode のバージョンアップが行われたため、下記のリンク先の github 上で marubatsu.ipynb を見た場合に、エラーの表示に、<a href='vscode-notebook-cell:/c%3A/Users/ys/ai/marubatsu/016/marubatsu.ipynb#W0sZmlsZQ%3D%3D?line=7'>8\</a>
のような HTML のリンクが表示されるようになりました。marubatsu.ipynb をダウンロードして、自分の PC で VSCode 上で実行することで、エラーの表示が正しく表示されるようになります。
次回の記事
更新履歴
更新日時 | 更新内容 |
---|---|
2023/10/09 |
for y in range(3) を for x in range(3) に修正しました |