0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Pythonで〇×ゲームのAIを一から作成する その16 プログラムの修正と関数の副作用

Last updated at Posted at 2023-10-05

目次と前回の記事

実装の進捗状況と前回までのおさらい

〇×ゲームの仕様と進捗状況

  1. 正方形で区切られた 3 x 3 の 2 次元のゲーム盤上でゲームを行う
  2. ゲーム開始時には、ゲーム盤の全てのマスは空になっている
  3. 2 人のプレイヤーが遊ぶゲームであり、一人は 〇 を、もう一人は × のマークを受け持つ
  4. 2 人のプレイヤーは、交互に空いている好きなマスに自分のマークを 1 つ置く
  5. 先手は 〇 のプレイヤーである
  6. プレイヤーがマークを置いた結果、縦、横、斜めのいずれかの一直線の 3 マスに同じマークが並んだ場合、そのマークのプレイヤーの勝利とし、ゲームが終了する
  7. すべてのマスが埋まった時にゲームの決着がついていない場合は引き分けとする

仕様の進捗状況は、以下のように表記します。

  • 実装が完了した部分を 背景が灰色の長方形 で記述する
  • 実装の一部が完了した部分を、太字 で記述する

前回までのおさらい

「その 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 に初期化されたゲーム盤が代入される。この boardinitialize_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 行目で仮引数 a1 を代入し、3 行目で a の値を表示しているので上記のプログラムと同様に、必ず 1 が表示 されます。2 行目で行われる処理は、ローカル変数である仮引数に a にどのような値が代入されていたとしても、その値を利用することなく a1代入して上書きする 処理を行っています。従って、関数呼び出しの際に、どのような値を実引数に記述 したとしても、この関数の処理に 影響は与えません。つまり、仮引数 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 つの グローバル変数 でしか扱うことが出来ません が、修正後のプログラムでは、任意の数の ゲーム盤のデータを、任意の名前の変数 に代入することが出来ます。

下記のプログラムは、board1board2 という変数に、それぞれ 別の 初期化されたゲーム盤を 代入 し、board1 には (0, 1) のマスに 〇 を、board2 には (1, 0) のマスに × を配置しています。実行結果からわかるように、board1board2別々のゲーム盤を表している ことがわかります。

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 行目で グローバル変数 boardplace_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 を記述 しているため、グローバル変数 aassign_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 を記述しているので、グローバル変数 boardplace_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」の記事で説明しているので忘れた方は復習して下さい。

おそらくこの説明で意味が分かる人は少ないと思いますので具体例を挙げます。下記のプログラムは、初期化されたゲーム盤の 各行row0row1row2 という グローバル変数に代入 し、それらを使って グローバル変数 board に初期化されたゲーム盤を代入しています。

この場合は、board に代入されたデータを 構成する 3 つのオブジェクト をグローバル変数 row0row1row2 が参照しているので、boardrow0 などは、データを 部分的に共有 しています。

このような場合に 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 に代入 し、その boardplace_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 座標 のデータを扱うので、ローカル変数 である 仮引数の名前 に下記のように boardxy という名前を付けました。

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 座標 のデータを扱うので、下記のプログラムのように、仮引数の名前boardxy という名前を付けるのが自然でしょう。

マークを取り除くということは、(x, y) のマスを表す要素に 半角の空白文字を代入 すればよいので、remove_mark は下記のプログラムのように定義する事ができます。

def remove_mark(board, x, y):
    board[x][y] = " "

(x, y) のマスが空白の場合にこの処理を実行すると、空白のマスを空白にするという処理が行われるので、問題は発生しません。そのため place_mark と異なり、remove_mark では if 文による空白のマスであるかどうかのチェックを 行う必要はありません

place_markremove_mark はどちらも ゲーム盤x 座標y 座標 を表すデータを代入する boardxy という 同じ名前の仮引数 を持ちますが、それらは 名前は同じ ですが 別の変数 です。

異なる変数に 対して 必ず異なる名前 を付けなければ ならない 場合は、place_markremove_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 座標 を表すデータを 入力 とする関数を さらに定義 する場合は、その関数の仮引数の名前を、上記の boardxyboard2y2y2 以外の名前にする必要 があります。さすがに、それは大変すぎますし、そのような関数が増えれば増えるほど、既に定義した関数の仮引数に使った名前と 重複しない 変数名を考えることは 困難になります

名前空間の仕組み があれば、関数呼び出しのたびに 新しいローカル名前空間が作成 されます。同じ名前 のローカル変数であっても、その変数名を管理する 名前空間が異なれば異なるオブジェクトに対応づけられる ので、異なる関数 の中で 同じ名前のローカル変数 を使っても、それらを 異なる変数 として 区別できる ようになります。

そのため、ゲーム盤x 座標 と y 座標 を表すデータを仮引数とする関数は boardxy という 共通の名前 の仮引数を使って関数の定義を行うことが出来るようになります。従って、下記のよう、同じプログラムの中に、同じ名前 の仮引数を持つ、place_markremove_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) から取り出して利用していますが、その際に取り出した値を xy という名前の グローバル変数に代入 しています。また、ゲーム盤のデータは、board という名前の グローバル変数に代入 しています。

異なる変数に 対して 必ず異なる名前 を付けなければ ならない場合 は、この 3 つのグローバル変数 xyboard と、place_mark の仮引数である ローカル変数 xyboard の名前をそれぞれ 異なる名前にする必要 がありますが、それは 大変 だと思いませんか?

名前空間の仕組みがあれば、グローバル変数と、ローカル変数は、同じ名前であっても異なる変数 として扱われます。従って、上記のプログラムのように、同じ名前 のグローバル変数とローカル変数を記述しても、それらは 別の変数として区別される ので、プログラムは 正しく動作 します。

名前空間の仕組みがあることで、以下のようなメリットが得られる。

  • 他の関数 のブロックの中で使われた ローカル変数の名前気にすることなく関数のブロックの中ローカル変数の名前を付ける ことが出来るようになる。

  • 関数の外で使われた グローバル変数の名前気にすることなく関数のブロックの中ローカル変数の名前を付ける ことが出来るようになる。

  • すべての関数 のブロックの中で使われた ローカル変数の名前気にすることなく関数のブロックの外グローバル変数の名前を付ける ことが出来るようになる。

異なるモジュールで同じ名前を使えるようにする

名前空間が必要となる他の状況として、ファイルに保存 された 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) に修正しました
  1. global 以外にも、nonlocal 変数名 と記述することで、その名前をローカル変数でない変数とすることができます。nonlocal については、必要になった時点で説明します

  2. イミュータブルなデータとは、オブジェクトが管理するデータを 後から書き換えることができない データの事です。詳細は「その 11」の記事を参照して下さい

  3. ミュータブルなデータとは、オブジェクトが管理するデータを 後から書き換えることができる データの事です。詳細は「その 11」の記事を参照して下さい

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?