LoginSignup
0
0

Pythonで〇×ゲームのAIを一から作成する その14 名前空間とスコープの仕組み

Last updated at Posted at 2023-09-28

目次と前回の記事

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

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

  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 の 名前空間スコープ について学ぶ必要があります。今回の記事では、名前空間とスコープの 仕組み について説明し、次回の記事で名前空間とスコープに関する補足説明、上記のプログラムのバグの原因と修正方法について説明します。

今回の記事はかなり複雑な内容に思えるかもしれませんが、実際には 見た目ほどは複雑ではありません。比較的 単純なルール を覚えれば 複雑さを緩和 することが出来るようになります。その点については次回の補足説明で説明します。今回の記事の内容がうまく理解できなくても、プログラミングの技術が向上すれば理解できるようになると思いますので、理解できなかった人後で この記事を 読み直してみる と良いでしょう。

名前空間とスコープ

Python の名前空間とスコープについてより詳しく知りたい方は、下記の Python の公式のドキュメントを見て下さい。ただし、このドキュメントの内容を理解するためには、ある程度の知識が必要 になります。

名前空間の意味

上記のリンク先で説明されているように、名前空間(namespace)とは、プログラム内に記述した 名前からオブジェクトへ対応づけ(mapping) のことを表します。

Python では、この名前に相当するものには、変数名関数名 など1 がありますが、今回の記事では最初は 変数名に限定 して 名前空間の説明 を行います。

「その 7」の記事で説明したように、Python では、プログラムが扱う データ を、オブジェクト という形式で 管理 しています。従って、プログラム内に記述した変数名からオブジェクトを対応づけるということは、変数名から、変数に 代入したデータ対応づける ことを意味します。

具体的には、a + 1 のような 変数名を記述 した際に、その a代入されたデータ を使って 計算が行われる のは、名前空間の仕組み を使って、a という 名前対応づけられたオブジェクト探し出し、そのオブジェクトが管理するデータを取り出すという処理が行われているからです。

つまり、名前空間は「変数名から、Python の中でオブジェクトの形式で管理されている 特定のデータ対応づける」という、変数の機能を実現 する 仕組み です。

対応づけには 方向性 があり、片方向 の対応づけと、双方向 の対応づけの 2 種類 に分類することが出来ます。Python の名前空間は 片方向 の対応づけで、名前 から オブジェクト という方向の対応づけが行われますが、オブジェクトから名前へという方向の対応づけは 行われません

また、対応づけは 1 対 11 対 多多 対 1多 対 多 の 4 つに分類することが出来ます。Python の名前空間の場合は、名前に対応づけられたオブジェクト1 つだけ ですが、オブジェクト複数の名前から対応づけられる 場合があるので 多 対 1 の対応づけです。

名前空間の仕組み

「その 7」の記事で説明したように、変数オブジェクトの id を格納 することで、数値型、文字列型、list などの任意の データを管理 する オブジェクト参照 します。この 参照 は、変数名からオブジェクトへの 対応づけ意味する ので、この 仕組み名前空間 の仕組み そのもの です。

参照 という用語は、参照元と参照先があることからわかるように、方向性を含めた対応づけ同様の意味 を持ちます。対応づけと同様の意味を持つ用語に、関連付け という用語があります。

分かりづらいと思いますので、図で説明します2。以前の図では、個別の変数を 1 つの直方体で表現し、その直方体の中にオブジェクトの id を表示していましたが、今回の記事からは、下図のように、変数と、その変数が格納するオブジェクトの id を 表の形式で図示 することにします。ただし、オブジェクトの id が 参照 するオブジェクトを 赤い点線で結ぶ 点は以前の図と同様です。

実際には、名前空間を表すデータもオブジェクトで管理されています。その点については今後の記事で詳しく説明します。

下図は、a = 0b = 1 という代入文を実行した場合の名前空間とオブジェクトの図です。

名前からオブジェクトへの対応づけは、下図では 名前空間の表 を使って、「名前→オブジェクトの id →オブジェクト」の手順で行われます。

上記をまとめると以下のようになります。

名前空間とは、プログラムに 記述された名前から、その名前が表す データを管理するオブジェクト対応づける対応表のような仕組み の事である。

関数オブジェクト

Python では、関数名名前空間 が扱う 名前の一つ です。そのように思えないかもしれませんが、変数名と関数名はいずれも 同じ仕組み名前から特定のオブジェクト対応づけられます。そのことを理解するためには、関数オブジェクト について理解する必要があります。

コンピュータは、数値や文字列など、あらゆるデータ2 進数のデジタルデータ表現 します。関数の定義 も、数値型の 1 などと同様に、プログラムの中に文字を使って記述することができるので、データの一種 として考えることができます。実際に、Python では、関数の定義2 進数のデジタルデータで表現 され、関数を定義すると そのデジタルデータを管理する オブジェクトが 新しく作成 されます。そのような、関数の定義を表すデータ を管理するオブジェクトのことを、関数オブジェクト と呼びます。

Python では、関数の定義 が記述されている文が 実行される と、以下のような処理が行われます。

  1. 関数の定義を表すデータ を管理する関数オブジェクトが 新しく作成される
  2. 関数の定義で記述した 関数名同じ名前の変数作成 され、その中に新しく作成された 関数オブジェクトの id が格納 される

上記の説明からわかるように、関数の定義 では、変数に関数を表すデータを代入 するという処理が行われます。つまり、代入文関数の定義 は、いずれも 代入処理 が行われるということです。変数と関数の違いは、代入された データ関数の定義を表すデータ であるかどうかの 違い でしかありません。

このことを、実際にいくつかのプログラムで確認することにします。

関数に対応づけられたオブジェクトの id を表示する例

下記のプログラムでは、前回の記事で紹介した place_mark という 関数を定義 していますが、この処理は place_mark という名前の 変数 に、関数オブジェクトの id を格納 する処理です。従って、下記のプログラムのように、id(place_mark) を記述することで、place_mark に対応づけられた関数オブジェクトの id を取得することができます。

def place_mark(x, y, mark):
    if board[x][y] == " ":
        board[x][y] = mark
    else:
        print("(", x, ",", y, ") のマスにはマークが配置済です")

print(id(place_mark))

実行結果

2180912782432

変数に関数を直接代入する例

関数 は、変数の一種 なので、別の変数 に代入文を使って 関数を代入する ことができます。下記のプログラムは、1、2 行目で、「2 つの仮引数 xy の合計を計算して返り値として返す処理」を行う add という名前の関数を定義しています。その後の 4 行目で、sum という異なる名前の 変数 に、add代入 することで、sumadd同じ関数オブジェクトを共有 するようになります。そのため、sumadd同じ機能を持つ関数 になり、sum を使って 関数呼び出しを記述する ことができます。実際に、5 行目のように sum(1, 2)print で表示すると 3 が表示されます。

def add(x, y):
    return x + y

sum = add
print(sum(1, 2))

実行結果

3

前回の記事で、関数呼び出しを記述する際に、実引数が存在しない場合 でも 必ず () を記述しなければならない と説明した理由はここにあります。() を記述しなかった場合は、関数の定義に記述されたブロックの 処理を実行する という関数呼び出し ではなく、関数に代入された 関数の定義を表すデータ を表すことになるからです。

現実の世界で例えると、関数呼び出しは「マニュアルに書かれている 指示に従って行動する事」、関数の定義を表すデータは「マニュアル そのもの」に相当します。

関数と同じ名前の変数にデータを代入する例

関数は、関数の定義を表す データの一種 で、関数名と同じ名前変数代入 されます。従って、関数と同じ名前の変数別のデータを代入 して 上書き することができます。下記のプログラムは、4 行目で 関数と同じ名前add という変数に数値型の 1 を代入 しています。その結果、add関数ではなくなり、5 行目のように add を表示すると 1 が表示されます。

def add(x, y):
    return x + y

add = 1
print(add)

実行結果

1

上記のプログラムは、関数が 変数の一種であることを示す ために記述したものです。実際には、関数に、関数以外のデータを代入して上書きするような処理は、特別な理由がない限り、ほとんど記述することはありません。

関数オブジェクトに関するまとめ

関数オブジェクトに関する内容をまとめると以下のようになります。

  • 関数の定義を実行 すると、関数の定義を表すデータ を管理するオブジェクトが 新しく作成される
  • そのようなオブジェクトの事を 関数オブジェクト と呼ぶ
  • 関数の定義に記述された 関数名と同じ名前の変数 が作成され、その中に 関数オブジェクトの id が格納 される
  • 従って、関数の定義が記述された文を実行すると、代入文と同じ処理 が行われる
  • 変数と関数の違いは、代入されたデータが、関数の定義を表すデータであるかどうか である
  • 関数は変数と同じ性質を持つので、他の変数に代入 したり、他の値を代入 することができる

このように、変数名と関数名は、同じ仕組み でオブジェクトに 対応づけられる ので、以後は「名前」という 用語 を、変数名と関数名の両方を指す という意味で使用します。

名前空間のスコープ

ここまでの説明で、名前空間という用語の中の「名前」に関する説明を行いました。次は「空間」という用語の意味について説明します。

空間を goo 辞書で引くと以下のような説明が記述されています。

物理学で、物体が存在し、現象の起こる場所

この説明は物理学での「空間」という用語の意味ですが、「名前空間」という用語から、「名前」に関する空間であることを考慮すると、この説明を「名前が存在し、名前に関する処理が行われる場所」のように読み替えることができます。

名前が存在する場所 は、Python のプログラム なので、名前空間の「空間」は、Python のプログラム を表すことになります。

先程の定義から、「空間」という用語には「場所」という意味があります。場所を goo 辞書で引くと以下のような説明が記述されています。

「何かが存在したり行われたりする所。ある広がりをもった 土地」

この説明の中の、「ある広がりをもった」という説明から、「場所」には 範囲がある ことがわかります。このことから、名前空間には 範囲がある ことが読み取れます。

上記のことから、名前空間が「プログラムの中で、変数名からオブジェクトへの対応づけ を、適用する範囲」の事を表すことがわかります。この名前空間を 適用する範囲 のことを、スコープ 3と呼びます。

名前空間の種類

名前空間にスコープがあるということは、異なるスコープ を持つ 複数の名前空間存在する ということを意味します。実際に、Python の名前空間には、以下のような種類があります。

  • Python のプログラム全体 を範囲とし、組み込み 関数などの名前を管理する「組み込み名前空間
  • モジュールのプログラム全体 を範囲とする「グローバル4名前空間
  • 関数5の仮引数と、関数のブロック を範囲とする「ローカル6名前空間

ビルドイン名前空間 以外 の名前空間は、Python のプログラムの実行中に、新しく作成 されたり、必要がなくなった時に 削除 されたりします。

以下に、それぞれにの名前空間について説明します。

組み込み名前空間

組み込み名前空間 は、これまでのプログラムでも利用してきた、printid などの あらかじめ定義 されている 組み込み(built in)定数や関数の名前 から、その データを管理するオブジェクト対応づけ ます。組み込み定数や関数は Python のプログラムの どこからでも利用できる ので、組み込み名前空間の スコープ は、Python の プログラム全体 です。

論理型の TrueFalse や、データが存在しないことを表す None組み込み定数 です。変数ではなく、定数 と呼ばれるのは、これらの組み込み定数に別の値を代入することが できない からです(代入しようとするとエラーになります)。

組み込み名前空間は、Python のプログラムを 実行した直後自動的に作成 され、プログラムの処理が終了するまで 存在し続けます。また、組み込み名前空間が管理する 名前の一覧 は、プログラムの実行中に 変化することはありません

Python の組み込み定数と関数の一覧については、下記のリンク先を参照して下さい。

グローバル名前空間

グローバル名前空間 は、モジュールに対して作られる ものなので、最初にモジュールについて説明します。

モジュール は、ファイルに記述 された Python のプログラム のことを表します。

Python のプログラムが記述された ファイルを実行 した場合は、そのプログラムの事を メインモジュール と呼びます。また、メインモジュールには、__main__ という名前が付けられます。

これは、VSCode などで JupyterLab のファイル にプログラムを 記述して実行 する場合も 同様 で、そのプログラムは メインモジュール になります。

他のファイル に記述されたモジュールを自分のプログラムに 組み込んで利用する ことを、モジュールを インポート すると呼びます。モジュールのインポートの記述方法については必要になった時点で説明します。

グローバル名前空間は、プログラムがモジュールを利用する際に、モジュールごとに新しく作られます。具体的には、以下の場合に新しいグローバル名前空間が作成されます。

  • Python の プログラムを実行 すると、メインモジュール のグローバル名前空間が新しく作成される
  • モジュールを インポート すると、そのモジュールの グローバル名前空間が新しく作成される

従って、Python のプログラムを実行すると、メインモジュール のグローバル名前空間を 含む、少なくとも 1 つ以上 のグローバル名前空間が 作られます

グローバル名前空間は、基本的にはプログラムの 処理が終了するまで存在し続けます

グローバル名前空間の スコープ は、モジュールのファイル に記述された Python の プログラム です。

JupyterLab に記述して実行したプログラムは、プログラムを実行する 仮想環境を選択した時点で開始 され、その際に 組み込み名前空間 と、メインモジュールのグローバル名前空間作成 されます。

その後で JupyterLab に記述して 実行したプログラム は、新しく実行されるのではなく、それまでに記述したメインモジュールのプログラムの 続きとして実行 されるので、その際にグローバル名前空間が新しく作成されることは ありません

JupyterLab で実行を開始したプログラムは、JupyterLab の ファイルを閉じた時点で終了 し、その際に 全ての名前空間が削除 されます。

他にも、JupyterLab のタブの上部にある「再起動」ボタンをクリックすることで、JupyterLab で実行されたプログラムを、ファイルを閉じることなく 強制的に終了 することができます。「再起動」ボタンを押した後で、JupyterLab に記述された プログラムを実行する と、新しい Python のプログラムが実行 され、組み込み名前空間 と、メインモジュールのグローバル名前空間新しく作成 されます。

ローカル名前空間

ローカル名前空間 は、主に 関数呼び出し が行われた際に 新しく作られ、関数呼び出しの処理が 終了した時点で削除 されます。

他にも、クラスの定義を実行した際にローカル名前空間が作成されます。その点については、今後の記事でクラスについて説明する際に、詳しく説明します。

ローカル名前空間の スコープ は、その関数の定義の中の 仮引数 と、関数のブロックの中に記述されたプログラム です。関数名 はローカル名前空間の スコープに含まれない点 に注意して下さい。

ローカル名前空間は、関数の定義の際に作られるの ではなく関数呼び出しが行われるたび に作られる点に注意して下さい。同じ関数 に対して 何度も関数呼び出しが行われた場合 に、その関数に対するローカル名前空間を 使いまわす ことは ありません。同じ関数であっても、関数呼び出しが 行われるたび に、何度でも ローカル名前空間が 新しく作られ、関数呼び出しの処理が 終了した時点で廃棄されます

このことは、今後の記事で説明する予定の、再起呼び出し という処理を正しく理解するために重要となります。

Python では、関数のブロック がローカル名前空間のスコープになりますが、for 文などのブロックはローカル名前空間のスコープにはなりません。一方、C 言語や、JavaScript などのプログラム言語では、関数のブロックだけではなく、任意のブロックローカル名前空間のスコープ になるものがあります。それらの言語を学んだことがある方は、混同しないように注意して下さい。

名前空間の種類と性質のまとめ

名前空間の種類と性質を表にまとめると以下のようになります。

名前空間 作成時期 存続期間 スコープ
組み込み名前空間 プログラムを実行した時 プログラムの実行が終了するまで Python のプログラム全体
メインモジュールのグローバル名前空間 プログラムを実行した時 プログラムの実行が終了するまで 実行したプログラムが記述されたファイル
それ以外のグローバル名前空間 モジュールをインポートした時 プログラムの実行が終了するまで インポートしたモジュールのファイル
ローカル名前空間 関数呼び出しが行われた時 関数呼び出しの処理が終了するまで 関数の仮引数と関数のブロック

名前空間のスコープの入れ子構造

名前空間のスコープは下記のような、入れ子の構造 になっています。なお、「ローカル名前空間のスコープ」のような表記は長いので、以後は「ローカルスコープ」のように、名前空間を省略して表記します。また、「組み込み名前空間のスコープ」は、一般的に「組み込みスコープ」ではなく、「ビルトイン7スコープ」と表記されるようなので、本記事でもそれに倣うことにします。

ビルトインスコープ > グローバルスコープ > ローカルスコープ

下図は、名前空間の スコープの入れ子構造 を図示したものです。下図は、marubatsu.py というファイルに Python のプログラムを記述して実行した場合の図です。また、その際に marubatsu.py では、xxx.py と yyy.py という別のファイルに記述された Python のモジュールを利用しているものとします。なお、図の xxx.py と yyy.py の中に記述したプログラムに 特に意味はありません

図からわかるように、それぞれの名前空間のスコープは以下のような構成になっています。

  • ビルトインスコープ は、すべての Python のプログラムを 含む
  • グローバルスコープ は、Python のプログラムを記述した モジュールごとに作られる
  • ローカルスコープ は、関数の定義ごとに作られる。ただし、関数の名前 はローカルスコープには 含まれない

Python では、関数のブロックの中に 別の関数を定義 することができます。そのような場合は、ローカル名前空間の 中に、別の関数のローカル名前空間が 入れ子 になった状態で作成されますが、話が複雑になるので、今回の記事ではそのような場合のことは説明しません。そのような場合の具体例については、必要になった時点で説明します。

名前空間に関する表記

上図のように、グローバル名前空間複数存在 する場合で、それらを 区別する 必要がある 場合 は、以後は「〇〇モジュールのグローバル名前空間」のように表記します。ただし、このような表記は長くなるので、必ず存在 する メインモジュールのグローバル名前空間 のことを、以降は 単にグローバル名前空間」と表記します。また、「グローバルスコープ」と 単に表記 した場合も、メインモジュールの グローバル名前空間の スコープ の事を表すことにします。

ローカル名前空間を 区別する 必要がある 場合 も同様に、関数の名前をつけて「place_mark のローカル名前空間」のように表記しますが、この表記も長いので、ローカル名前空間を 区別する必要がない 場合は、以後は 単にローカル名前空間」や「ローカルスコープ」と表記します。

名前の衝突と名前解決

先程の図からわかるように、プログラムの文は、最低でも ビルトインスコープと、グローバルスコープの 2 つ のスコープに 属しています。それに加えて、関数の仮引数と、関数のブロックの中の文は、その関数の ローカルスコープにも 属します。

プログラムの文が 複数の名前空間 のスコープに 属する ということは、その文に記述された 名前からオブジェクト を、どの名前空間 を使って対応づけるかを 決める必要がある ということです。

名前空間は、それぞれ 個別に 名前とオブジェクトの 対応づけ を行います。そのため、異なる名前空間 が、同じ名前を管理 する場合があります。例えば、以下のような状況が発生する可能性があります。なお、このような状況が発生する具体例については、この後で実際に紹介します。

  • グローバル名前空間が、a という名前から 0 というデータを管理するオブジェクトを対応づける
  • ローカル名前空間が、a という名前から 1 というデータを管理するオブジェクトを対応づける

上記のような状況では、プログラムに記述された a が、グローバル名前空間によってオブジェクトに対応づけられるか、ローカル名前空間によって対応づけられるかによって、その値が 0 であるか、1 であるかが 異なります。このように、名前が記述された文を スコープとする複数の名前空間同じ名前を管理 するような状況の事を、名前の衝突 と呼びます。

名前の衝突が起きた場合は、その名前を、どの名前空間を使ってオブジェクトに対応づけるかを決める必要があります。そのような、複数の名前空間 の中から、1 つ の名前空間を 選択して 名前からオブジェクトを 対応づける処理 の事を、名前解決8 と呼びます。

名前解決 には、厳密な手順 が決まっており、その手順について説明します。

代入処理による名前空間への登録と更新

変数に値を代入するという処理は、変数名から、変数に代入するデータを管理する オブジェクト対応づける 処理です。従って、代入処理 が行われると、名前空間に対して「変数名からオブジェクトへの対応づけ」の 登録、または更新 が行われます。

その際に、対応づけの登録または更新は、代入先の変数名 が記述されている 文をスコープ とする 入れ子 になった 名前空間 のうち、最も内側にある 名前空間に対して行われます。

組み込み名前空間 以外名前空間 は、作成された時点では 中身は空 になっています。従って、変数に対して 初めて 代入処理が行われた場合は 登録 が、そうでない場合は 更新 が行われます。

Python では、= 演算子以外にも、下記の場合に 代入処理と同様の処理 が行われます。従って、下記の処理を実行した 場合にも、対応づけの登録または更新が行われます.

  • 関数の定義を実行 した場合に、関数名の変数 に対して 関数の定義を表すデータ が代入される
  • 関数呼び出し を行った場合に、仮引数実引数 が代入される

上記をまとめると、以下のようになります。

Python では下記の文を実行した場合に 代入処理 が行われる

  • = 演算子による代入文
  • 関数の定義
  • 関数呼び出し

代入処理 が行われた場合は、以下のような手順で処理が行われる

  1. 代入先の変数名 が記述されている 文をスコープ とする 入れ子 になった 名前空間 のうち、最も内側にある 名前空間を 選択 する
  2. 選択した名前空間の中から、代入先の変数名を探す
  3. 見つからなかった場合 は、選択した名前空間に「代入先の変数名から、代入する値を管理する オブジェクト への 対応づけ」を 登録 し、見つかった場合更新 する

この説明では意味が分からない人もいるかと思いますので、少し後で具体例と図を使って説明します。

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

名前が記述された式 を実行した場合は、その 名前解決 は以下の手順で行われ、対応づけられたオブジェクトから データが取り出されて式の計算 が行われます。

  1. 名前が記述されている 式をスコープ とする 入れ子 になった 名前空間 のうち、最も内側にある 名前空間を 選択 する
  2. 選択した名前空間の中から 名前を探す
  3. 名前が 見つかった 場合は、その名前空間を使って、名前からオブジェクトを 対応づける
  4. 名前が 見つからなかった 場合は、一つ外側 の名前空間を 選択 して、手順 2 へ戻る
  5. 手順 4 で、外側の名前空間が 存在しない場合 は、NameError という エラーが発生 する

式の中 に記述された 名前解決 の手順をまとめると、以下のようになります。

内側の 名前空間から 順に 名前を探し、最初に見つかった 名前空間を使って名前からオブジェクトを 対応づける

名前の呼び方と名前のスコープ

名前解決を行うことによって、プログラムに記述された 名前 を、どの名前空間を使ってオブジェクトに対応づける かが決まります。そこで、以降は変数や関数などの 名前が、どの名前空間を使って対応づけられるかが 明確になるように、「組み込み関数」、「グローバル変数」、「ローカル変数」のように表記します。

また、プログラムに記述した 名前 は、その名前に対応する 名前空間のスコープ内でのみ 同じオブジェクトに対応づけられます。そこで、プログラムの中で、名前が 同じオブジェクト に対応づけられる 範囲 のことを、「変数のスコープ」のように表記します。

同じ名前であっても、異なる名前空間 によってオブジェクトに 対応づけられる 場合は、異なるものです。このことは、非常に重要なこと なので、忘れないようにして下さい。

名前解決の具体例

言葉だけではわかりにくいと思いますので、名前解決で行われる処理を、図を使いながら、具体例を挙げて説明します。

下記のプログラムは、前回の記事で、関数の処理を説明する際に使ったプログラムです。

def assign_one(x):
    x = 1

a = 0
assign_one(a)
print(a)

実行結果

0

このプログラムの関数の仮引数の名前を x から a に変更しても、行われる処理は全く変わりません が、プログラムで行われる処理が初心者にとっては急に わかりづらくなります

下記のプログラムは、上記のプログラムの関数の仮引数の名前を x から a に変更 し、3 行目に print(a) を追加した ものです。Python のプログラムの初心者がこのプログラムを見た場合、6 行目で assign_one を呼び出した後で、2 行目で a1 が代入されるので、3 行目の
print(a)1 が表示されるのと同じように、7 行目の print(a) でも 1 が表示されると 勘違い する人が多いのではないかと思いますが、実際には 修正前 のプログラムと 同じ ように、0 が表示 されます。

このプログラムを実行した際に行われる処理を 1 行ずつ名前空間の様子を図示 しながら説明します。

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)
修正箇所
- def assign_one(x):
+ def assign_one(a):
-     x = 1
+     a = 1
+     print(a)

a = 0
assign_one(a)
print(a)

実行結果

1
0

プログラムの開始直後の図

下図は、このプログラムを marubatsu.py というファイル9に保存して実行を開始した時点での名前空間とオブジェクトの状態を図示したものです。なお、このプログラムでは、他のモジュールをインポート しない ので、グローバル名前空間は 1 つしか存在しません

図では、左に プログラムと名前空間のスコープ を、真ん中にそれぞれの 名前空間の内容 を、右にプログラムの実行によって作成された オブジェクト を表示します。

プログラムが実行された直後の状態では、printid などの 組み込み関数 の定義を管理する オブジェクトが作成 されており10、組み込み名前空間の中には図のように print という 名前オブジェクトの id を記録することによって、名前からオブジェクトへの対応づけ が行われています。

図では省略 していますが、組み込み名前空間には id などの、他の組み込み関数の名前からオブジェクトへの対応づけも存在しています。また、それらのデータを管理するオブジェクトも存在しています。

marubatsu.py のプログラムは まだ 1 行も実行されていない ので、図の グローバル名前空間 には、何も登録されていません

また、ローカル名前空間は、関数呼び出しが行われた際に作成されるので、図には ローカル名前空間 はまだ 存在していません

1 ~ 3 行目の関数の定義の処理

下図は、プログラムの 1 ~ 3 行目の assign_one の定義を実行 した直後の状態を図示したものです。以降は、プログラムの中の 新しく実行 した部分と、名前空間の中で 更新、または 変更 された部分を 赤い文字と水色の背景色 で表記します。

先ほど説明したように、関数の定義 が記述された行が 実行 されると、関数と 同じ名前の変数 に、関数の定義を表す データが代入 されるという処理が行われることを思い出してください。

代入処理 が行われた場合の処理の手順を以下に再掲します。

  1. 代入先の変数名 が記述されている 文をスコープ とする 入れ子 になった 名前空間 のうち、最も内側にある 名前空間を 選択 する
  2. 選択した名前空間の中から、代入先の変数名を探す
  3. 見つからなかった場合 は、選択した名前空間に「代入先の変数名から、代入する値を管理する オブジェクト への 対応づけ」を 登録 し、見つかった場合更新 する

上記の手順を行う前に、assign_one の関数の定義を管理する、上図の ID が 852 のオブジェクトが 新しく作成 されます。その後で、上記の手順に従って、assign_one の関数の定義の処理は、以下のような手順で行われます

  1. 代入先の 変数名 は、assign_one で、最も内側 にある名前空間は グローバル名前空間 である
  2. グローバル名前空間の中に、assign_one存在しない
  3. グローバル名前空間の中に、「assign_one という 名前からassign_one の関数の定義を管理する オブジェクト への 対応づけ」を 登録 する

上記の手順を行った結果、上図のように、グローバル名前空間 に「assign_one という 名前から と、assign_one の関数の定義を管理する オブジェクト への 対応づけ」が 登録 されます。従って、assign_oneグローバル関数 です。

上図では、1 ~ 3 行目が ローカルスコープ を表すオレンジ色の枠で囲まれているので、ローカルスコープのように 見えるかもしれません が、ローカル名前空間が作成されるのは、関数呼び出しが実行された時 です。関数の定義は、関数呼び出しとは 異なる処理 なので、この時点ではローカル名前空間は 存在しません

5 行目の代入処理

下図は、プログラムの 5 行目の a = 0 実行した直後の状態を図示したものです。

代入文が記述されているので、a という名前に対して、先ほどの assign_one の場合と 同様の手順代入処理 が行われます。その結果、上図のように 0 を管理する ID が 199 のオブジェクトが新しく作成され、グローバル名前空間に「a という 名前からa に代入されたデータを管理する オブジェクト への 対応づけ」が登録されます。従って、この aグローバル変数 です。

6 行目の式の処理

6 行目には、assign_one(a) という が記述されており、式の中には assign_onea という 2 つの名前 が記述されています。そのため、この 2 つの 名前解決 を行う必要があります。

名前が記述された式 を実行した場合の 名前解決の手順 を再掲します。

  1. 名前が記述されている 式をスコープ とする 入れ子 になった 名前空間 のうち、最も内側にある 名前空間を 選択 する
  2. 選択した名前空間の中から 名前を探す
  3. 名前が 見つかった 場合は、その名前空間を使って、名前からオブジェクトを 対応づける
  4. 名前が 見つからなかった 場合は、一つ外側 の名前空間を 選択 して、手順 2 へ戻る
  5. 手順 4 で、外側の名前空間が 存在しなかった場合 は、NameError という エラーが発生 する

上記の手順に従って、assign_one の名前解決は、以下のような手順で行われます

  1. assign_one が記述された式をスコープとする、最も内側 にある名前空間は グローバル名前空間 である
  2. グローバル名前空間 の中に、assign_one登録されている
  3. グローバル名前空間 から、assign_one に対応づけられたオブジェクトの id が 852 であることがわかる

assign_one名前解決 を行った結果、assign_one に対応づけられたオブジェクトが管理する、関数の定義のデータを利用 することが出来るようになります。

a についても assign_one同様の手順名前解決 が行われ、a に対応づけられたオブジェクトが管理する 0 という数値型のデータを利用 することが出来るようになります。

式の中に記述された すべての名前解決が完了 したので、名前解決によって得られたデータを使って、assign_one(a) という 関数呼び出しの処理を開始 します。

前回の記事で、関数を 定義する前関数呼び出しを実行 すると エラーになる と説明しました。その理由は、関数の名前は、今回の記事で説明したように、関数の定義が実行された時 に、名前空間に 登録 されるからです。下記のプログラムのように、関数の定義を実行するよりも 前に 関数呼び出しを実行すると、その時点ではその 関数の名前 はグローバル名前空間にも、その外側にある組み込み名前空間にも 登録されておらず、名前解決を 行うことができない ため NameError というエラーが発生します。

a = 0
assign_one(a)

def assign_one(a):
    a = 1

実行結果

---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[1], line 2
      1 a = 0
----> 2 assign_one(a)
      4 def assign_one(a):
      5     a = 1

NameError: name 'assign_one' is not defined

なお、上記のプログラムは、JupyterLab の上部にある 「再起動」ボタンをクリックしてから 実行して下さい。再起動せずに上記のプログラムを前のプログラムに続けて実行すると、前のプログラムassign_one定義されている ので エラーは発生しません

1 行目の仮引数に実引数を代入する処理

関数呼び出しが実行されると、最初に 関数の仮引数 に、関数呼び出しの 実引数の値が代入 されます。具体的には、仮引数のa = 実引数のa のような代入処理が行われます。

下図は、プログラムの 1 行目の仮引数 a に、6 行目の実引数 a を代入した直後の状態を図示したものです。ローカル名前空間 は、関数呼び出し が行われた 直後に作成 されます。下図の真ん中の下に 新しい ローカル名前空間が 表記 されるようになったのは、そのことを表しています。

代入処理 が行われるので、仮引数の a という 名前 に対して、先ほどの assign_one や 5 行目の a の場合と 同様の手順 で処理が行われます。ただし、先ほどの場合と異なり、仮引数の a が記述されている行の、最も内側 にある名前空間は、ローカル名前空間 です。

ローカル名前空間は、関数呼び出しによって 作成された直後 なので、この時点では 中身は空 になっています。従って、ローカル名前空間には a という名前は 登録されていません

上記の事から、「仮引数 a に 実引数 aを代入」する処理によって、上図のように ローカル名前空間 に「a という名前からa に代入された グローバル変数 である 実引数 a が参照するオブジェクト への 対応づけ」が 登録 されます。従って、仮引数の aローカル変数 です。

上図からわかるように、a という名前 はグローバル名前空間と、ローカル名前空間の 両方に登録 されていますが、それぞれの名前からオブジェクトへの 対応づけ は、異なる名前空間 に登録されているので、「グローバル変数 a」と「ローカル変数 a」は、名前は同じ でも、異なる変数 です。この時点ではこの 2 つの変数は、同じオブジェクトを共有していますが、この後の 2 行目の処理によって、この 2 つの 同じ a という名前の変数 に、異なる値が代入 されます。

2 行目の代入処理

代入処理 が行われるので、代入する値である 1 を管理する ID が 620 のオブジェクトが新しく作成されます。

2 行目を スコープ とする 最も内側 にある名前空間は、ローカル名前空間 なので、ローカル名前空間の中から a という名前を探し、先ほど登録したばかりの a が見つかります。そのため、「ローカル名前空間の a という名前から オブジェクトへの 対応づけ」が、図のように a に代入された値を管理するオブジェクトに 更新 されます。

その結果、グローバル変数 a とローカル変数 aデータの共有が解除 されます。図からわかるように、変更されるのはローカル変数 a だけで、グローバル変数 a の値は変更されません。

このことからも、グローバル変数 aローカル変数 a同じ名前 でも 異なる変数 であることを確認できます。

関数呼び出しの処理の終了

これで assign_one の関数呼び出しの処理が終了しますが、関数呼び出しの処理の中で return 文 が実行されなかったので、この関数の返り値はデータが存在しないことを表す None になります。

そのため、6 行目の assign_one(a)None に置き換わりますが、6 行目は 代入文ではない ので、置き換わった後で何らかの 処理が行われることはありません

また、関数呼び出しの処理が 終了 したので、ローカル名前空間 はこの時点で 削除 されます。その結果、ローカル変数 a利用 することは できなくなります

下図は、関数呼び出しの処理が終了した直後の状態を図示したものです。

上図のように、図から ローカル名前空間削除 されています。なお、ローカル名前空間の表にあった a に対応づけられていたオブジェクトは、どの名前からも 対応づけられなくなった ので、図から削除 しました。

7 行目の式の処理

7 行目の式には、printa という 2 つの名前 が記述されているので、その 2 つの名前を解決 する必要があります。

最初に、a の名前解決から説明します。7 行目をスコープとする 最も内側 にある名前空間は、グローバル名前空間 なので、グローバル名前空間の中から a という名前を探し、ID が 199 のオブジェクトに 対応づけられている ことがわかります。その結果、そのオブジェクトが管理する 0 という数値型のデータ を利用することが出来るようになります。

次に print の名前解決について説明します。a と同様に、グローバル名前空間 の中から、print という名前を探し ますが、見つけることが出来ません。そこで、次にグローバル名前空間のすぐ外にある 組み込み名前空間 から print探す ことになります。ビルドイン名前空間には、print が登録 されているので、print に対応づけられたオブジェクトを見つけることができます。従って、print組み込み関数 です。

print に対応づけられたオブジェクトと、a 対応づけられたオブジェクトが管理するデータを使って、print(a) という関数呼び出しの処理を開始します。結果として、画面に グローバル変数 a の値である 0 が表示 されます。

次回の記事について

以上が上記のプログラムで行われる処理の説明ですが、この説明を読んだ方は、プログラムに 名前を記述するたびに、名前解決がどの名前空間によって行われるかを、上記で説明したような手順に従って 調べる必要があるのか と、うんざりした人がいるかもしれません。また、名前空間は複雑なだけで、メリットが感じられない という人がいるかもしれません。

実際には、名前解決がどの名前空間によって行われるかは、プログラムを 工夫して記述する ことで、比較的簡単に見分ける ことが出来るため、あまり 心配する必要はありません。今回の記事で図を使って名前解決の処理の手順について詳しく説明したのは、ある程度 複雑なプログラム を自分で記述したり、他人が書いたプログラムを 理解する 際に、名前解決の 仕組みを正しく理解 しておくことが 重要になる場合 があるためです。

次回の記事では、名前解決がどの名前空間によって行われるかを 見分ける手順 と、名前解決を 見分けやすい プログラムの 記述方法 について説明します。

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

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

次回の記事

更新履歴

更新日時 更新内容
2023/9/29 for y in range(3) → for x in range(3)
ブロック → 関数のブロック
ビルトイン名前空間 → 組み込み名前空間
「関数と同じ名前の変数にデータを代入する例」の説明の修正
  1. 他にも本記事ではまだ説明していない「クラス名」や「モジュール名」などがありますが、いずれも 同じ仕組み で特定の オブジェクトと対応 づけられます

  2. 図でのオブジェクトなどの表記の仕方については、「その 7」の記事を参照して下さい

  3. scope は「範囲」を表す英単語です

  4. global は「全体的な」を表す英単語です

  5. 関数以外にも、今後の記事で説明するクラスを範囲とする場合があります

  6. local は「局所的な」を表す英単語です

  7. built in は「組み込み」を表す英単語です

  8. 名前解決という用語は、ネットワークでドメイン名をIPアドレスに対応づけるという意味でも良く使われます。同じ用語でも本記事の用語とは意味が異なる点に注意して下さい

  9. JupyterLab で、marubatsu.ipynb というファイルに同じプログラムを記述して実行した場合でも、本記事と同様の処理 が行われます

  10. 組み込み関数の定義の内容は 見ることができない ので、図では print の定義を管理するオブジェクトの中に「print 関数の定義」と表記しました

0
0
3

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