LoginSignup
0
0

Pythonで〇×ゲームのAIを一から作成する その15 名前解決を見分けやすいプログラムの記述方法

Last updated at Posted at 2023-10-01

目次と前回の記事

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

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

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

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

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

前回までのおさらい

前々回の記事では、「ゲーム盤の (x, y) のマスにマークを配置する」関数と、「ゲーム盤を初期化する」関数を定義し、それらの関数が正しく動作するかどうかを下記のプログラムを実行して確認しました。

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 ) のマスにはマークが配置済です
[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]

実行結果から、ゲーム盤の初期化を行う initialize_board を呼び出したにも関わらず、その次の place_mark(0, 1, "〇") を実行すると「( 0 , 1 ) のマスにはマークが配置済です」という表示が行われました。そのため、上記のプログラムの どこかにバグが存在する ことがわかります。

前回の記事ではこのバグが起きる原因を理解するために 必要な知識 として、Python の 名前空間スコープ の仕組みについて説明しました。

今回の記事では、名前空間とスコープに関する処理のまとめと、名前解決を 見分けやすい プログラムの 記述方法 について説明します。今回の記事の内容も、上記のバグが起きる原因を理解したり、バグを修正するために必要な知識になります。

今回の記事も長くなったため、上記のバグの原因と修正方法について説明することができませんでした。上記のプログラムのバグの原因と修正方法については次回の記事で説明します。

名前空間とスコープに関する処理のまとめ

前回の記事で説明した、『名前空間への「名前からオブジェクトへの対応づけ」の登録、更新』の手順と、「式の中に記述された名前の名前解決」の手順は、一見するとかなり複雑そうに 見えるかもしれません が、実際には それほど 複雑ではありません

それぞれで行われる処理をまとめると以下のようになります。

名前空間への「名前からオブジェクトへの対応づけ」の登録、更新

  • 名前からオブジェクトへの対応づけは、以下の 代入処理1 によって行われる
    • = 演算子 による代入処理
    • 関数の定義の実行 の際に行われる、関数名と同じ名前変数 に、関数の定義 を表す データを代入 する処理
    • 関数呼び出し の際に行われる、仮引数に実引数を代入 する処理
  • 対応づけは、代入処理をスコープとする名前空間のうち、最も内側の名前空間 に対して登録または更新が行われる
  • 名前空間に対応づけが登録されていなければ 登録 され、そうでなければ 更新 される

要約すると、「名前空間への対応づけの登録、更新は、代入処理 が行われた際に、最も内側の名前空間 に対して行われる」のようになります。

このことから、関数の ブロックの中 に記述した 代入文 によって、グローバル変数 に値を 代入 する ことはできない ということがわかります2

式の中に記述された名前の名前解決

  • 式をスコープとする、最も内側の名前空間 を使って、名前からオブジェクトへの対応づけが行われる
  • 名前空間に名前が登録されていない場合は、内側 の名前空間 から順番 に名前空間を探す
  • どの名前空間にも 名前が登録されていない 場合は、NameError という エラーが発生 する

要約すると、「名前解決は、最も内側の名前空間 から順番に名前を探し、最初に見つかった オブジェクトに対応づける」のようになります。

名前の見分け方

上記で説明したまとめから、プログラムに記述された 名前 が、どの名前空間によって管理 されているかを、下記の手順で見分けることが出来ます。

  1. 代入処理代入先の変数の名前 は、最も内側 の名前空間が管理する2

  2. 関数の定義 によって記述された 関数名 は、基本的に グローバル関数 である3

  3. 変数名がどの名前空間によって管理されるかを、以下の手順で見分けることが出来る

    • 関数の ブロック内 に記述された 変数 の場合
      1. 仮引数と同じ名前 の変数は、ローカル変数 である
      2. 関数のブロック の中で 代入文によって値が代入された 変数は、ローカル変数 である
      3. 上記以外の変数で、関数の定義の外代入文によって値が代入された 変数は グローバル変数 である
      4. 上記以外の変数で、組み込み名前空間に登録されていれば 組み込み定数 または 組み込み関数 である
      5. 上記以外の場合は NameError という エラー が発生する
    • 関数の 定義の外 に記述された 変数 の場合
      1. 関数の定義の外代入文によって値が代入された 変数は グローバル変数 である
      2. 上記以外の変数で、組み込み名前空間に登録されていれば 組み込み定数 または 組み込み関数 である
      3. 上記以外の場合は NameError という エラー が発生する

なお、関数の定義の外 の場合の 手順 1 ~ 3 は、関数のブロック内 の場合の 手順 3 ~ 5同じ手順 なので、上記の手順は見た目ほどは複雑ではありません。

また、それぞれの名前空間のスコープは、頭の中で下図のようなイメージを行うことで比較的簡単に把握することが出来ます。

1.png

図からわかるように、最も内側の名前空間 は、以下の表で判別することが出来ます。

プログラム 最も内側の名前空間
関数の仮引数 ローカル名前空間
関数のブロック ローカル名前空間
上記以外 グローバル名前空間

上記の表と、「ローカル名前空間の外にグローバル名前空間」、「グローバル名前空間の外に組み込み名前空間」、「組み込み名前空間の外にはなにもない」ということを理解していれば、名前解決で行われる処理が それほど複雑ではない ことが実感できるのではないでしょうか?

名前解決に類似する現実の処理

ここまでの説明を読んでも、名前解決で行われる処理が、得体のしれない複雑な処理を行っているように見えるかもしれないので、我々が 普段の生活 で行っている、名前解決に類似する処理 を紹介します。

プログラムで 異なる名前空間同じ名前が登録 される ことがある ように、現実世界でも 異なる集団 に、同じ名前 の人が 存在する 場合があります。

人間にとっての 集団 には様々なものが考えられますが、一般的には 最も身近 な集団は 家族 でしょう。また、家族より広い集団としては、近所の知り合い、学校のクラス、会社の同僚など、様々なものがあります。今回は、身近な順 に、「家族」、「近所の知り合い」、「日本人」という 3 つの集団を使って説明します。

ある集団 の中の 会話 で、誰かの 名前だけ4 を出した場合、最初は その集団の中 から 名前の心当たりを思い浮かべ、心当たりがなければ、集団の範囲を 少しずつ広げていく のが一般的ではないでしょうか?

例えば、家族 の会話 で「一郎」という 名前だけ を出した場合、以下のような手順で人物を思い浮かべる(名前から人物を対応づける)のが一般的5ではないかと思います。

1.家族 に「一郎」という名前の人がいれば、その人を思い浮かべる
2. いない場合は、近所の知り合い に「一郎」という名前の人がいれば、その人を思い浮かべる
3. いない場合は、日本人 の有名人である野球選手の「イチロー」6を知っていれば思い浮かべる
4. 上記のいずれでもなければ、 誰も思い浮かべることが出来ない

また、近所の知り合い の会話で「一郎」という名前だけを出した場合は、上記の手順 2 から心当たりを探すのが一般的ではないかと思います。

人間は、頭の中 に「家族の名簿」、「近所の知り合いの名簿」、「日本人の名簿」のようなものを 記憶しており、それぞれの名簿の中に 同じ名前の人物がいても、上記のような手順で、名前を特定の人物に対応づける ことが出来ます。この、頭に記憶した 複数の名簿から、名前と人物を 対応づける 上記の 手順 は、Python の 名前解決 で行われる 手順似ている と思いませんか?

下記の表は、この例えと名前空間の用語の対応を示したものです。

例え 名前空間
家族の名簿 ローカル名前空間
近所の知り合いの名簿 グローバル名前空間
日本人の名簿 ビルドイン名前空間
家族 関数
近所の知り合い モジュール
日本人 プログラム全体
人物名 変数名

このように考えることで、名前解決で行われる処理は、特に不思議なことを行っているわけではない ことが理解できるのではないでしょうか?

他にも、この例えと、名前空間とスコープには以下のような類似点があります。

例え 名前空間
家族の名簿などの 名簿 には、その名簿を 適用できる 人物の 範囲 がある 名前空間には スコープ がある
田中さんの家の「一郎」さんと、佐藤さんの家の「一郎」さんのように、異なる「家族」同じ名前 の人物がいても 区別 できる 異なる関数 であれば、同じ名前ローカル変数異なる変数 として 区別 できる
「近所の知り合い」、「学校の友人」、「会社の同僚」など、集団が異なれば、その中に 同じ名前 の人物がいても 区別 できる 異なるモジュール であれば、同じ名前グローバル変数異なる変数 として 区別 できる

なお、以前の記事でも述べましたが、身近なものを使った例えは、物事をわかりやすく説明できるという利点がありますが、例えと、例えられたものは 全く同じものではありません。名前空間と上記の例えは似てはいますが、全く同じ性質を持つわけではない 点に注意して下さい。

上記の例えは、名前解決で行われる処理が 理解できない人 に、処理の イメージを掴む ことが出来るようにするために紹介しました。イメージがつかめた方は、今一度、名前解決で行われる処理の手順を読み直して、正しい知識を身に付ける ようにして下さい。

名前の種類を見分けやすいプログラムの記述方法

ここまでの説明を聞いても、名前がどの名前空間で管理されているかを 判定する手順複雑 だと思っている人が多いかもしれません。

他人が記述 したプログラムを 理解 する際は、どうしても 先ほどの手順 で名前がどの名前空間で管理されているかを 判定する必要 があります。一方、自分でプログラムを記述する 場合には、名前がどの名前空間で管理されているかが、見分けやすくなる ようなプログラムの 記述方法 があります。

名前の種類

ここまでの記事では、「変数名」と「関数名」という 2 種類の名前について説明しました。

Python の名前には他にも「クラス名」と「モジュール名」があります。この 2 つについては、必要になった時点で説明することにしますので、今回の記事では取り上げません。

また、名前空間には「組み込み名前空間」、「グローバル名前空間」、「ローカル名前空間」の 3 種類 があります。

これらを組み合わせることで、名前を「組み込み定数7、「組み込み関数」、「グローバル変数」、「グローバル関数」、「ローカル変数」、「ローカル関数」の 6 種類に分類 することができます。

この 6 種類の名前簡単に見分ける ことが出来るプログラムの 記述方法 について説明します。

iffor などの 予約語 はプログラムの中で 特別な意味 を持つ キーワード で、予約語を変数名などの 名前に使うことはできません。また、予約語は変数名などのように、オブジェクトに対応づけられた 名前ではない ので、どの 名前空間 にも 登録されません。Python の予約語の一覧については下記のリンク先を参照して下さい。

組み込み定数

下記のリンク先の組み込み定数の一覧を見て下さい。

一覧からわかるように、組み込み定数の 数は少なく、その中で 主に使われる のは、論理型のデータ を表す TrueFalse、と、データが存在しないこと を表す None3 つ です。

組み込み定数は、他の名前と異なり、どの名前空間 であっても、代入処理 を行うことは できませんエラーが発生 します)。従って、これらの名前は どこに記述されていた場合でも 必ず組み込み定数 です。

上記の 3 つ の組み込み定数を 覚えておけば組み込み定数 であるかどうかを 見分けることは容易 です。また、この 3 つは 頻繁に使われる ものなので、嫌でも覚えることになるでしょう。

TrueFalseNone の 3 つは常に 組み込み定数 である。

組み込み関数

printid などの 組み込み関数 をプログラムで 利用する場合 は、それらの名前が組み込み関数の名前であることを 理解しながら プログラムを 記述しているはず です。従って、自分が 良く使う 組み込み関数の名前がプログラムに記述されている場合は、その名前が組み込み関数の名前であることを 見分けることは難しくない と思います。

下記のリンク先の、組み込み関数の一覧 を見ればわかりますが、組み込み関数は 約 80 もの種類があり、そのすべてを 覚えるのは大変 です(筆者もすべてを暗記しているわけではありません)。

出来る限り、プログラムに記述する 名前 は、組み込み関数の名前と被らない ような名前に するべき ですが、意図せずに 組み込み関数と 同じ名前の変数に値を代入 するようなプログラムを記述することがあるかもしれません。そのようなプログラムを記述して実行しても、エラーは発生しません。単に、その組み込み関数をプログラムで 使用できなくなる だけです。

例えば、下記のプログラムは、1 行目で id という 組み込み関数print で表示しています。組み込み関数を print で表示 すると、下記の実行結果のように「<built-in function xxx>」(xxx の中には組み込み関数の名前が入ります)が表示されます。

その後の 2 行目で id = 1 という代入処理を行うことにより、id はグローバル変数 になります。従って、3 行目で printid を表示すると、1 が表示されます。また、このプログラムは 2 行目以降では id という 組み込み関数を利用できなくなります

print(id)
id = 1
print(id)

実行結果

<built-in function id>
1

上記のような現象が起きるのは、2 行目の id = 1 によって、グローバル名前空間 に、id という名前 から 1 を管理するオブジェクトへの 対応づけが登録 されるためです。3 行目の id の名前解決を行うと、グローバル名前空間id という名前が 登録されている ため、1 を管理するオブジェクトに対応づけられます。

プログラムを 記述した本人 にとっては、組み込み関数と同じ名前の変数に値を代入した際の デメリット は、その組み込み関数を利用できなくなる だけ のように 思えるかもしれません が、実際には下記のような デメリット があるため、組み込み関数と同じ名前の変数に値を代入することは 避けたほうが良い でしょう。

  • 最初は使わないと思っていた組み込み関数を、後で使いたくなった時に困る ことになる
  • 他人が そのプログラムを 見たとき に、プログラムの 意味が非常に分かりづらくなる

なお、自分のプログラムで 使いたくなるような名前 の組み込み関数は あまり多くない ので、そのような名前に 絞って 組み込み関数の 名前を覚えておけば良い のではないかと思います。

個人的には、allanyidinputlenlistmaxminsetsumtype あたりの名前が 間違って変数名や関数名に使いそうな名前 だと思いますので、それらの名前を覚えておいて、値を代入しないように気を付けて下さい。人によっては他の名前も使いたくなる可能性があるので、先ほどのリンク先の組み込み関数の名前の一覧を ざっと眺めて確認しておく ことをお勧めします。なお、その際に組み込み関数の処理の内容までを覚える必要はありません。

間違って値を代入しそうな組み込み関数の 名前を憶えておき、その名前に 値を代入 したり、その名前の 関数を定義 したり しない ようにすることで、名前が 組み込み関数であるかどうかを見分ける ことが出来る。

上記の説明を記述している時に気づいたのですが、筆者も「その 13」と「その 14」の記事で、うっかり組み込み関数 sum と同じ名前の関数を定義していました。それらの記事を訂正しても良いのですが、うっかり 組み込み関数と 同じ名前を使ってしまう実例 としてあえてそのまま残しておくことにします。

組み込み関数の名前に値を代入した後でも、その組み込み関数を利用する方法があります。

組み込み関数は、ビルトインモジュール内で定義 されており、import builtins と記述することで、ビルトインモジュールを インポート することが出来ます。

インポートしたモジュール内で定義された関数は、モジュールの名前.関数名 と記述することで利用することが出来ます。

下記のプログラムは id に 1 を代入した後で、builtins.id を記述することで、組み込み関数 idprint で表示しています。

import builtins

id = 1
print(builtins.id)

実行結果

<built-in function id>

なお、モジュールのインポートの方法と、モジュールの中で定義された名前の利用方法については、必要になった時に詳しく説明します。

ローカル関数

これまで紹介したプログラムでそのような記述を行ったことはありませんが、関数のブロックの中関数の定義を記述 することが出来ます。関数のブロックの中で 代入処理 を行うと、代入先の変数が ローカル変数 になるのと 同様 に、関数のブロックの中で定義 された関数は ローカル関数 になります。以前の記事で説明した「関数の定義 を実行すると、関数と 同じ名前の変数 に、関数の定義を表す データを代入 するという処理が行われる」というこをと思い出してください。

ローカル関数 は、ローカル変数と同様 に、その 関数のブロックの中だけでしか利用できません

下記のプログラムは、関数 printAブロックの中 で、関数 printB を定義 しています。

1  def printA():
2      print("A")
3      def printB():
4          print("B")
5      printB()
6
7  printA()
8  printB()
行番号のないプログラム
def printA():
    print("A")
    def printB():
        print("B")
    printB()

printA()
printB()

実行結果

A
B
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[3], line 8
      5     printB()
      7 printA()
----> 8 printB()

NameError: name 'printB' is not defined

3、4 行目で定義 されている printB は、printAローカル関数 なので、printAブロックの中 である 5 行目から 呼び出すことが出来ます。実行結果の 2 行目に表示される B は、5 行目から呼び出された printB によって表示されたものです。

しかし、printAブロックの外 の 8 行目では、printB名前解決を行うことが出来ない ので、8 行目を実行すると NameError という エラーが発生 します。

ローカル関数は 特別な目的 で記述するものなので、一般的には記述することは あまりありません。従って、当面の間は、上記のプログラムの意味がわからなくても気にする必要はないでしょう。ローカル関数に関しては必要になった時点で説明します。

ただし、インデントを間違える などの理由で、間違って 関数のブロックの中で関数の定義を記述してしまうことが原因で エラーが発生する 場合があるので、その点には注意して下さい。

本記事でも、必要がない限りローカル関数の定義は行いません。また、今回の記事では、以降の内容は、ローカル関数をプログラムに記述しない ものとして説明を行います。

関数のブロックの中 で関数を 定義しないようにする ことで、ローカル関数 のことを 気にする必要がなくなる

グローバル関数

上記で説明した ローカル関数を プログラムの中に 記述しなければ、組み込み関数 以外の全ての関数 は、グローバル関数 になります。本記事 でも特別な場合を除いて、グローバル関数のみを定義 します8。また、これまでの記事でローカル関数を定義したことは 一度もありません

関数は 何らかの処理を行う ものなので、関数名place_mark のように、英語の 動詞を含めた名前 にすることで、見ただけで 関数名であるかどうかを 判別できる ように 工夫する ことが出来ます。

また、関数名は主に 関数の定義関数呼び出し の際に記述されます。関数の定義の場合は、関数名の直前に def が記述 され、関数呼び出しの場合は、関数名の直後に ( が記述 されるので、何れの場合も、関数の名前であるかどうかは容易に区別することが出来ます。

VSCode で関数を定義した場合は、その 関数名の上にマウスカーソルを移動 すると、関数を表す英単語である 「function」が記述されたメッセージが表示 されるので、これを見て関数であるかどうかを 確認することも出来ます

下図は、下から 2 行目の assign_one の上にマウスカーソルを移動した場合の図で、assign_one のすぐ上に、function が記述されたメッセージ9が表示されています。

プログラムの中にローカル関数を記述しない」、「関数の名前を工夫する」の 2 点を守ることによって、名前が グローバル関数 であることを簡単に 見分ける ことが出来る。

グローバル変数とローカル変数

ここまでで、組み込み定数、組み込み関数、グローバル関数、ローカル関数の見分け方について説明しました。次に、それ 以外の名前 である、グローバル変数とローカル変数 の見分け方について説明します。

グローバル変数とローカル変数の見分け方

代入文 では、代入先の変数 は、代入文をスコープとする 最も内側の名前空間 に名前が登録される2ので、以下のように、グローバル変数とローカル変数を 簡単に見分ける ことが出来ます。

  • 関数のブロックの外 に記述された 代入文 の場合は、グローバル変数 である
  • 関数の 仮引数 と、関数のブロックの中 に記述された 代入文 の場合は、ローカル変数 である

また、関数のブロックの外 に記述された 式の中の変数 は、グローバル変数 です。

ここまでは 簡単に見分けることが出来ます が、問題は 関数のブロックの中 に記述された 変数 で、今回の記事の最初で紹介した 少し複雑な手順 に従って、その名前がグローバル変数かローカル変数かを 見分ける必要 があります。その際に、 の中に 記述された名前だけ を見てその 判定を行うことが出来ない 事が 複雑さの原因 になっています。

この 複雑さを解消 するプログラムの 記述方法 について説明します。

関数のブロックの中の変数を見分けやすいプログラムの記述方法

問題を複雑にしているのは、関数の ブロックの中 にグローバル変数とローカル変数が 混在する 点なので、関数の ブロックの中ローカル変数のみを記述する ようすることで、この 複雑さを解消 することが出来ます。

そのようなプログラムは、以下の 条件を守る ことで記述することが出来ます。

関数の ブロックの中の変数 は、仮引数 か、その関数の ブロックの中で値を代入した変数のみ を記述する。

上記の条件を守って関数の定義を記述すると、代入文と式区別をすることなく グローバル変数と、ローカル変数を 関数のブロックの内外 で完全に 分離する ことが出来ます。その結果、ローカル変数とグローバル変数を以下のように簡単に見分けることが出来るようになります。

グローバル変数とローカル変数の見分け方。

  • 関数の外 に記述した変数は、すべてグローバル変数 である
  • 関数のブロックの中 に記述した変数は、すべてローカル変数 である

図で示すと、下図の オレンジ色の範囲 に記述された変数が ローカル変数オレンジ色の外 に記述された変数が グローバル変数 になります。

1.png

上図は、「その 14」の記事で使った図を再掲したものです。そのため、上図のプログラムは、上記の条件に従ってプログラムが 記述されていない ので、オレンジ色の枠の中にグローバル変数が記述されています。実際に place_mark の関数ブロック内に記述されている board はグローバル変数です。

グローバル変数とローカル変数を分離するメリット

グローバル変数と、ローカル変数を 関数のブロックの内外で分離 するということは、関数の 入力 である 実引数 と、出力 である 返り値 以外の手段で、関数のブロックの内外の データの受け渡し を行うことが 出来ない という事です。このことには、変数を見分ける 以外 にも 大きなメリット があります。

関数の中へのデータの受け渡しを実引数に限定するメリット

関数の 外部から 関数の 中へデータの受け渡し実引数に限定 することで、その関数のブロックの中で行われる 処理の結果出力 は、実引数 によって関数に入力されたデータ のみ によって 定まる ようになります。別の言葉でいうと、実引数以外の要因 で、関数の 処理の結果出力変化しない ということです。そのような関数は、入力処理の結果出力因果関係明確 になるというメリットがあります。

ほとんどの物事には 例外 があります。例えば、関数のブロックの中で、キーボードからの入力に応じた処理を行う場合、乱数(サイコロの出目のようなでたらめな数のこと)を使って計算を行う場合、現在の時刻を表示する場合などでは、実引数以外の要因で処理の結果が変化します。

今回の記事の説明は、関数のブロックの中で、そのような処理を行わない関数についての説明だと思ってください。

一般的に関数は、入力 された データに応じた 処理と出力を行います。入力と 無関係な要因 で処理と出力が 変化する 関数は 使いづらい と思いませんか?

分かりづらいと思いますので、具体例を挙げます。下記のプログラムは、2 つの実引数の値の合計を計算し、その計算結果を返り値として返す add という名前の関数を定義しています。この関数を、同じ 実引数の 組み合わせ を記述して呼び出した場合は、関数のブロックの中で 同じ処理 が行われ、同じ値 を返り値として 返します。例えば、add(1, 2) の返り値は、どんな場合でも 必ず 3 になります。

def add(x, y):
    return x + y   # この x と y はローカル変数

print(add(1, 2))

実行結果

3

一方、下記のプログラムで定義された add_y という名前の関数は、実引数 の値と グローバル変数 y の値の合計を計算し、その計算結果を返り値として返す関数です。この add_y でも、下記のプログラムの 4、5 行目のように、グローバル変数 y2 を代入してから add_y(1) を呼び出すことで、add(1, 2) と同様に 1 + 2 の計算が行われ、3 が返り値として返ります。

しかし、add_y(1) の返り値は、下記の 6、7 行目の実行結果のように、実引数以外グローバル変数 y の値 によって 変化 します。

1  def add_y(x):
2      return x + y   # x はローカル変数だが、y はグローバル変数
3
4  y = 2
5  print(add_y(1))
6  y = 5
7  print(add_y(1))
行番号のないプログラム
def add_y(x):
    return x + y   # x はローカル変数だが、y はグローバル変数

y = 2
print(add_y(1))
y = 5
print(add_y(1))

実行結果

3
6

このような、実引数と関係のないグローバル変数の値 によって、処理の結果出力 が変わるような関数は、その関数を 利用する側の立場 から見ると、入力処理の結果出力因果関係がわからなくなる ため、関数が 扱いづらくなる という大きな デメリット があります。

現実の例で例えると、ボタンによって 砂糖の量 と、氷の量調整 することができるコーヒーの自動販売機を思い浮かべて下さい。この自動販売機の 入力 は、押したボタンによって決まる「砂糖の量」と「氷の量」で、処理 は「コーヒーと砂糖と氷を混ぜること」、出力 は「混ぜ合わせたコーヒー」です。

この自動販売機を利用する人は、砂糖の量と氷の量のボタンを 同じ組み合わせで押した場合 は、毎回同じ 分量の砂糖と氷が入った コーヒー が出てくることを 期待する はずです。

この自動販売機が、ボタン以外の、例えば周囲の気温などの 利用者が知らない要因 で、出てくるコーヒーの 銘柄が変わってしまう 場合、そのような自動販売機は使いづらいと思いませんか?

関数の ブロックの中で利用 する、関数の外部のデータ は、特別な理由 がある場合を 除いて実引数 によって 関数の仮引数に渡されたデータのみ を使ったほうが良い。

本記事でも、特別な理由がない限り、関数をそのように定義する事にします。

関数の外へのデータの受け渡しを返り値に限定するメリット

関数は、「入力 するデータ」、「行われる 処理」、「出力 されるデータ」について 理解 していれば、具体的に関数の中でどのような処理が行われているかを 知らなくても利用できる という利点があります。例えば、これまで何度も利用してきた print という組み込み関数は、「実引数入力 したデータを、表示する という 処理10を行う関数ですが、この関数が具体的にどのような処理によって表示を行っているかについて 知る必要はありません

print のような、他人が定義 した関数を、関数の定義の プログラムを見ずに 利用する場合は、関数の処理の中で、グローバル変数に値を代入する ような処理が行われていたとしても、そのことを知ることはできません。他人が定義した関数を利用する際に、その関数が何らかの グローバル変数に値を代入する処理 を内部で行っていた場合11、その グローバル変数と同じ名前 を使ったプログラムの 動作がおかしなこと になってしまう可能性があります。

関数の外へのデータの受け渡し返り値に限定 することで、関数呼び出しの処理 によって、実引数と無関係グローバル変数 に勝手に値が 代入されることがなくなります

先程のコーヒーの自動販売機で例えると、関数の返り値に相当するのは、自動販売機の取り出し口に出てきたコーヒーです。自分で決めたグローバル変数 に関数の 返り値を代入 することは、取り出し口のコーヒー を、購入者が決めた人物受け取らせる ことに相当します。

一方、関数のブロックの中でグローバル変数に値を代入する処理は、コーヒーの自動販売機でコーヒーを購入した際に、自動販売機が 勝手に 自動販売機の 外にいる 近くに歩いている人などの、購入者が予期しない人 にコーヒーを 勝手に渡す ことに相当します。そのような自動販売機を使いたいと思いますか?

関数の副作用

上記のノートの説明で、「実引数と無関係 なグローバル変数に勝手に値が代入されることがなくなります」のように表記したのには理由があります。これまでに説明した方法では、関数の ブロックの中グローバル変数直接 値を 代入 することは できません が、例えば実引数が list の場合、その実引数の値を代入した 仮引数の要素に 値を代入することによって、実引数に関連する グローバル変数の 間接的変更する ことは できる からです。

関数呼び出しの処理によって、関数のブロックの外をスコープとする グローバル変数 の値が 変更 されることを、関数の 副作用 と呼びます。今回の記事で紹介した名前の解決を見分けやすいプログラムの記述方法は、関数の副作用を無くすことは できません が、関数の副作用を 制御する ことはできます。そのことと関数の副作用に関しては、次回の記事で詳しく説明します。

関数呼び出しを使ってグローバル変数に値を代入する例

グローバル変数と、ローカル変数を関数のブロックの内外で分離した場合に、関数呼び出しを使って、グローバル変数に直接値を代入 するためには、関数の ブロックの外 で、グローバル変数に 関数の返り値を代入 する必要があります。

下記のプログラムは、過去の記事で関数の処理を説明する際に紹介したプログラムです。このプログラムは、5 行目のグローバル変数 a に代入された値が assign_one という関数で 1 に変更されると誤解されやすい例として紹介しました。2 行目の処理によって、5 行目のグローバル変数 a の値が 1 に変更されないのは、2 行目のローカル変数 a と、5 行目のグローバル変数 a同じ名前 であっても 異なる変数 だからです。

1  def assign_one(a):
2      a = 1
3      print(a)
4
5  a = 0
6  assign_one(a)
7  print(a)
行番号のないプログラム
def assign_one(a):
    a = 1
    print(a)

a = 0
assign_one(a)
print(a)

実行結果

1
0

上記のプログラムを、下記のプログラムのように、4 行目で ローカル変数 a を関数の 返り値とし、7 行目で グローバル変数 a に関数の 返り値を代入 することで、ローカル変数 a の値である 1 を、グローバル変数 a受け渡す ことが出来ます。その結果、8 行目を実行すると 1 が表示されます。

1  def assign_one(a):
2      a = 1
3      print(a)
4      return a
5
6  a = 0
7  a = assign_one(a)
8  print(a)
行番号のないプログラム
def assign_one(a):
    a = 1
    print(a)
    return a

a = 0
a = assign_one(a)
print(a)
修正箇所
def assign_one(a):
    a = 1
    print(a)
+    return a

a = 0
- assign_one(a)
+ a = assign_one(a)
print(a)

実行結果

1
1

まとめ

以下の 条件を守って プログラムを記述することで、名前の種類を簡単に見分ける ことが出来るようになる。

  • 組み込み関数と 同じ名前を使わない ようにする
  • 関数の ブロックの中関数の定義を記述しない ようにする
  • 関数の名前place_mark のように、動詞など を使って 動作を表すような名前 にする
  • 関数の ブロックの中の変数 は、仮引数ブロックの中で値を代入した変数のみ を記述する

上記の条件を守ることで、名前を以下の手順で見分けることが出来る。

  1. TrueFalseNone組み込み定数 である
  2. id など、変数の名前として 間違って使いそうな 組み込み関数の 名前を覚えておく ことで、組み込み関数 の名前を 見分ける ことが出来る
  3. 動作を表すような名前 は、グローバル関数 である
  4. 関数の定義の外 に記述された 変数 は、グローバル変数 である
  5. 関数の 仮引数 と、関数の ブロックの中 に記述された 変数 は、ローカル変数 である

本記事でも、特別な場合を除いて上記の 条件を守ったプログラムを記述 します。

「関数の ブロックの中の変数 は、仮引数 か、ブロックの中で値を代入した変数のみ を記述する」という条件を守った場合、関数の内外で ローカル変数とグローバル変数を 分離する ことができ、以下のようなメリットが得られる。

  • 入力処理の結果出力因果関係明確 になる
  • 関数呼び出しの処理 によって、実引数と無関係グローバル変数 に勝手に値が 代入されることがなくなる

次回の記事について

次回の記事では、initialize_board のバグの原因と修正方法などについて説明した後で、関数の副作用と、名前空間が必要になる理由について説明します。

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

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

次回の記事

更新履歴

更新日時 更新内容
2023/10/09 for y in range(3)for x in range(3) に修正しました
  1. 他にも、クラスの定義やモジュールのインポートの際などでも行われます。それらについては必要になった時点で説明します

  2. 実際には、global を使うことで、関数のブロックの中でグローバル変数に値を代入することができるという 例外があります が、global頻繁に使われるものではない ので、今回の記事では global使わないという前提 で話を進めます。global については次回の記事で詳しく説明します 2 3

  3. 本記事で紹介したプログラムにはまだ出てきていませんが、関数のブロックの中に関数を定義した場合は、その関数はローカル関数になります。ローカル関数については、この後で説明します

  4. 「野球選手の一郎」のように、名前を特定できるような情報を 含まず に、名前だけ を出した場合のことです

  5. これは、対応づけの手順の一例で、もちろん、これ以外にも名前を対応づける手順はあるでしょう

  6. もちろん、人によっては、別の「一郎」という名前の有名人を思い浮かべることはあるでしょう

  7. 組み込み定数は、変数のように 値を代入することが出来ない ので、組み込み変数ではなく、組み込み定数と呼ばれます

  8. まだ説明していない、クラスの定義の中に関数の定義を記述することは良くありますが、そのような関数は、メソッドという名前で一般的な関数と区別されます。メソッドについては必要になった時点で説明します

  9. 表示される「(function) def assign_one(a: Any) -> None」の意味については、今後の記事で説明します

  10. print返り値を返さない(厳密には None を返します)関数なので、出力はありません

  11. 次回の記事で説明する、global を使うことで、関数のブロックの中でグローバル変数に値を代入することができます

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