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を一から作成する その13 関数の定義と関数呼び出し

Last updated at Posted at 2023-09-24

目次と前回の記事

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

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

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

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

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

前回までのおさらい

前回の記事では、仕様の 3、4 を実装するために、「ゲーム盤の (0, 1) のマスが空いている場合に 〇 のマークを配置する」という処理を行う、下記のプログラムを記述しました。

board = [[" "] * 3 for x in range(3)]
if board[0][1] == " ":
    board[0][1] = ""
else:
    print("(0, 1) のマスにはマークが配置済です")
print(board)
if board[0][1] == " ":
    board[0][1] = "×"
else:
    print("(0, 1) のマスにはマークが配置済です")
print(board)

実行結果

[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
(0, 1) のマスにはマークが配置済です
[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]

他のマスや、× のマークに対しても 同様の処理 を記述すれば良いのですが、その度に上記のプログラムの 2 ~ 5 行目のような 4 行分のプログラムを記述するのは 大変 です。このような場合は、関数 を使って、指定したマスにマークを配置する処理を まとめる ことで、プログラムを簡潔に記述することができます。

Python の関数

関数とは何か

プログラム言語における関数は、入力 されたデータに対して、何らかの 処理 を行い、処理の結果を 出力 するような機能を持ちます。このように、関数は、入力処理出力3 つ で構成されます。

例えば、過去の記事で Python のオブジェクトを説明する際に使用した Python の id という関数は、以下の表のような、入力、処理、出力を持ちます。

入力 データ
処理 入力されたデータを管理するオブジェクトを探す
出力 入力されたデータを管理するオブジェクトの id

他人が作成した関数を利用 する際には、その関数の「入力」、「処理」、「出力」のそれぞれについて学ぶ必要がありますが、関数の処理が 具体的にどのような手順で行われるか について 知る必要はありません 。関数のように、中で行われている処理が外から見えないようなもののことを、黒い箱に例えて ブラックボックス と呼びます。

また、関数には、関数が 定義された後 は、プログラムの 好きな場所 から 何度でも利用 することができるという性質があります。

現実の世界でも、関数のような 性質を持つ ものは 身の回りにあふれています。例えば、自動販売機は、「お金」と「商品を選ぶ」という 入力 を行うと、自動販売機の中で何らかの 処理 が行われ、「商品」と「おつり」という 出力 が行われます。その際に、自動販売機の中で行われる処理が具体的にどのような手順で行われるかについて、知る必要はありません

他の例としては、車は、「ハンドル」、「アクセル」、「ブレーキ」という 入力 を行うと、車の中で何らかの 処理 が行われ、車を運転するという 出力 が得られます。その際に、車の運転の仕方さえ知っていれば、車の中でどのような手順で処理が行われているかについて知る必要はありません。

そう考えると、身の回りに関数のようなものがあふれていることが実感できるのではないでしょうか?

関数の定義

Python では関数を以下のように記述して 定義1します。

def 関数名(仮引数):
    関数の処理を記述するブロック

関数名 には、変数名と同様のルール2で自由に名前を付けることができます。

仮引数 は、関数に 入力されたデータ代入する変数 のことで、() の中に、必要な数だけ 半角の , で区切って 記述します。なお、入力が 一つもない 関数の定義の場合でも、関数名の後に 必ず () を記述 する 必要 がある点に注意して下さい。

関数の ブロックの記述方法 は、「その 5」の記事で説明した for 文のブロックと同様です。

関数の定義文の一種 なので、代入文や、for 文などと同様に、実行されます。ただし、関数の定義が実行 された際に、関数の ブロック に記述されたプログラムは 実行されない 点に注意して下さい。関数の ブロック に記述されたプログラムは、後述の 関数呼び出し という処理を行うことで、 実行されます

上記のことから、関数の定義が実行 された際に 何の処理も行われない ように 見えるかもしれません が、実際には、後述の 関数呼び出し を行うことが 出来るようにする という処理が行われます。

関数の定義の実行 は、処理の手順を書いたマニュアルを作成することに似ています。マニュアルを 作成しただけ では 何の作業も行われません。作成したマニュアルを誰かに渡して、「マニュアルの通りに作業を行ってくれ」という 指示を行う ことで、マニュアルの中に記述された 作業が行われます。このような指示を行うことが、後述の 関数呼び出し に相当します。

なお、Python では関数の定義時の 仮引数の記述方法 にはいくつかの バリエーション があります。今回の記事ではその中の 基本的な記述方法 について説明します。他の仮引数の記述方法については、必要になった時点で紹介します。

既に 値が代入された変数名 や、既に 定義された関数名同じ名前 の関数を定義すると、その名前は新しく定義された関数で 上書きされる ことになります。

下記のプログラムでは 1 行目で、a1 を代入しているので、2 行目を実行すると 1 が表示されます。

その直後の 4、5 行目で、同じ a という名前 を使って 関数を定義 しているので、a1 ではなく、新しく定義した関数を表す ようになります。そのため、7 行目を実行すると、print で関数を表示した時に表示される、 <function 変数名 at ・・・> のような表示3が行われます。

1  a = 1
2  print(a)
3
4  def a():
5     print(1)  # この処理に特に意味はありません
6
7  print(a)
行番号のないプログラム
a = 1
print(a)

def a():
   print(1)  # この処理に特に意味はありません

print(a)

実行結果

1
<function a at 0x00000269D7701DA0>

このように、関数を定義する際には、他の変数名や、関数名と 名前がかぶらない ように 気を付ける必要 があります。なお、このようなことが起きる理由については、今後の記事で説明します。

関数呼び出し

関数の定義を実行した後 のプログラムでは、関数呼び出し を記述して、その 関数の定義のブロック に記述されたプログラムを 実行する ことができます。Python では、関数呼び出しは、下記のように記述します。

関数名(実引数)

実引数 とは、関数に 入力するデータ のことで、関数名の直後の () の中に、実引数を表す を必要な数だけ 半角の , で区切って 記述します。関数に入力するデータが 存在しない 場合でも、関数名の後に 必ず () を記述 する 必要 がある4点に注意して下さい。

関数呼び出しが記述された文が実行されると、以下のような手順で処理が行われます。

  1. 実引数 に記述された 式の計算結果 が、記述された順番通り に、呼び出した関数の定義で記述した 仮引数に代入 する
  2. 呼び出した関数の定義の ブロック に記述されたプログラムを 実行する
  3. 関数の定義のブロックに記述された すべてのプログラムを実行する か、後述の return 文 が記述された文 が実行された時点で、関数呼び出しの処理が 終了 する
  4. 関数呼び出しが return 文で終了 した場合は、return 文に記述した 式の計算結果 が、関数が 出力するデータ となる。return 文で終了しなかった場合 は、データが存在しない という意味を表す None が関数が 出力するデータ となる。この関数が 出力するデータ のことを関数の 返り値5 と呼ぶ
  5. 文の中に記述した 関数呼び出しの部分 が、関数の 返り値に置き換わって 処理が続けられる

Noneデータが存在しない ことを表す、None 型 のデータです。None 型のデータは、None だけ です。None は を表す論理型の False とは 異なる用途 で使われます。

return 文は、関数呼び出しの処理を終了 し、式の計算結果関数の返り値 として返す処理を行う文で、下記のように記述します。関数の ブロックの途中 に記述された return 文が実行された場合は、残りのブロック の処理は 実行されない 点に注意が必要です。

return 

なお、Python では関数呼び出し時の 実引数の記述方法 には、仮引数の記述方法と同様にいくつかの バリエーション があります。今回の記事ではその中の 基本的な記述方法 について説明します。他の実引数の記述方法については、必要になった時点で紹介します。

関数の定義と関数呼び出しの具体例

関数の定義と関数呼び出しについて、具体例を挙げて説明します。

下記のプログラムの 1、2 行目では、入力 として 2 つのデータを 仮引数 ab で受け取り、その 合計を計算する という 処理 を行い、合計 を返り値として 出力 するという機能を持つ add という名前の関数を定義しています。

4 行目では、この関数の実引数に 12 という 6を記述して関数呼び出しを行っています。

def add(a, b):
    return a + b

print(add(1, 2))

実行結果

3

上記のプログラムを実行した際に行われる処理を以下の表に示します。

行数 行われる処理
1、2 関数の定義が実行 され、以後の処理でこの 関数を呼び出すことが出来る ようになります
4 print() の中に記述された、add(1, 2) という関数呼び出しの処理が開始されます
1 add(1, 2) に記述された 2 つの 実引数 である 12 が、add の関数の定義の 仮引数 に記述された ab に、その 順番通りに代入 されます。具体的には、a = 1b = 2 という 代入処理 が行われます
2 return 文が記述されているので、return の後に記述されている a + b が計算され、その 計算結果3 が関数の 返り値 となり、関数呼び出しの 処理が終了 します
4 add(1, 2) が、関数の 返り値3置き換わり ます。結果として、print(3) が実行され、3 が表示されます

上記の「関数の定義が実行され、以後の処理でこの関数を呼び出すことが出来るようになります」という説明は、どの関数の定義でも同じ なので、以後はこの説明は 省略 します。

また、上記の 4 行目その次の 1 行目 の、関数呼び出し が行われた 直後 の説明はかなり冗長なので、以後は「add が呼び出され、仮引数に対して a = 1b = 2 という代入処理が行われます」のように表記します。

実引数と仮引数

実引数と仮引数の意味の違い

実引数と仮引数を区別せずに、単に「引数」と記述することがありますが、この 2 つの用語はそれぞれ 異なる意味 を持つので、本記事では 区別して表記 します。実際に、英語では 実引数 のことを argument仮引数 の事を parameter のように 異なる用語 で表記します。

先ほど説明したように、実引数 は、関数に 入力するデータ のことで、 の形で記述します。一方、仮引数 は関数呼び出しで関数に渡された 入力データ代入する変数 のことです。どちらも用語に「引数」が入っているので同じようなものを表す用語だと 勘違いされがち ですが、実引数データ を表すのに対し、仮引数 はデータを代入する 入れ物(変数) を表します7

このように、実引数と仮引数は、性質が全く異なる ものを表す点に注意して下さい。

実引数は関数に 入力するデータ を表す 、仮引数はそのデータを代入する 変数 のことである。

実引数と仮引数の間で行われる処理

関数呼び出しが行われた際には、一つ一つの 実引数仮引数 の間で、先頭から記述された順 に、下記のような 代入処理 が行われます。

仮引数 = 実引数

このことと、「その 8」の記事以降で説明した Python で行われる代入処理 について 正しく理解 できていれば、関数呼び出しを説明する際に、誤解されがちな例 としてよく取り上げられる下記の 2 つの例で行われる処理を 正しく理解 できるはずです。

勘違いされがちな関数呼び出しの処理 その 1

下記のプログラムを実行すると、以下のような処理が行われると 勘違い して、1 が表示されると 勘違いする 人がいるかもしれませんが、実際には 0 が表示されます。

下記は、間違った 処理の手順です(間違っているので背景が赤いノートにしています)。

  1. 5 行目で assign_one の実引数に a を記述して呼び出しているので、assign_one の仮引数の xa と同じデータを共有するようになる
  2. 従って、x = 1 を実行すると、a の値も 1 になる (これが 間違い で、共有が解除される ので a の値は 変化しません
1  def assign_one(x):
2      x = 1
3
4  a = 0
5  assign_one(a)
6  print(a)
行番号のないプログラム
def assign_one(x):
    x = 1

a = 0
assign_one(a)
print(a)

実行結果

0

1、2 行目で定義されている関数は、仮引数に 1 (one) を代入 (assign) するという処理を行うので、assign_one という名前を付けました。このように、変数や関数に 複数の英単語を組み合わせた名前 を付ける際に、単語と単語の間を 半角の _ (アンダーバー)で 区切って 名前を付けるという 記法 があります。

他にも、assignOne のように、先頭以外 の単語の 頭文字を大文字 で表記するという記法があります。この方法は名前の大文字の部分がラクダのこぶのようにみえることから、キャメルケース(camel case)と呼ばれます。なお、先頭を大文字にしないのは、先頭を大文字にした名前が 別の用途で使われる からです。別の用途については必要になった時点で説明します。

どちらの記法を使っても構いませんが、混乱を避ける ため、一つのプログラム内では 共通の記法 を使うべきです。本記事では前者の _ を使う記法 を使うことにします。

実際には、以下の表のような処理が行われるので、上記のプログラムで assign_one(a) を呼び出しても、a の値は変化しません

行数 行われる処理
4 a0 が代入されます
5、1 assign_one が呼び出され、仮引数に対して x = a という代入処理 が行われます。この処理によって、xa同じデータを共有 するようになります
2 x = 1 という代入処理が行われます。x に値が代入されたことによって、xa のデータの 共有が解除 されます。その際に a の値は変化しません。return 文を実行せずに、関数のブロックの処理が終了したので、関数の返り値は None になります
5 assign_one(a) が、関数の返り値の None に置き換わりますが、この行では 代入処理は記述されていない ので、置き換わった後で 何の処理も行われません
6 a0 のまま値は変化していないので、0 が表示されます

まだピンとこない人がいるかもしれないので、上記のプログラムを、関数を使わない 同様の処理 を行うプログラムに修正した下記のプログラムを見て下さい。x = axa を代入した後で、x1 を代入しても、a の値は変更されません。

下記のプログラムは「その 7」の記事で紹介した プログラム A と同様の処理を行っています。下記のプログラムを見てもピンとこない人は、「その 7」から復習して下さい。

a = 0
x = a
x = 1
print(a)

実行結果

0

上記の修正前と修正後のプログラムは、厳密には 同じ処理を行っている わけではありません が、修正前のプログラムで行われている処理を 理解する という意味では 同じ処理 を行っていると 考えても構いません

なお、修正前と修正後のプログラムの違いは、それぞれのプログラムの後で、x に値が代入されているかどうか という点にあります。例えば、それぞれのプログラムの直後に print(x) を記述して実行すると、修正前のプログラムはエラー が発生しますが、修正後のプログラムは 1 が表示 されます。この違いを理解するためには、名前空間 という概念を理解する必要がありますが、今回の記事の内容を理解するだけであれば、名前空間を理解する必要はありません。

名前空間は重要な概念なので、次回の記事で詳しく説明します。

勘違いされがちな関数呼び出しの処理 その 2

下記のプログラムを実行すると、以下のような処理が行われると 勘違い して、[0, 1, 2] が表示されると考える人がいるかもしれませんが、実際には [1, 1, 2] が表示されます。

下記は、間違った 処理の手順です。

  1. 5 行目で assign_one の実引数に a を記述して呼び出しているので、assign_one の仮引数の x には a の値が複製8される(これが 間違い で、複製ではなく 共有 されます)
  2. 従って、xa の値は同じ内容を持つ異なる list なので、x[0] = 1 を実行しても、a の値は変化しない
1  def assign_one(x):
2      x[0] = 1
3
4  a = [0, 1, 2]
5  assign_one(a)
6  print(a)
行番号のないプログラム
def assign_one(x):
    x[0] = 1

a = [0, 1, 2]
assign_one(a)
print(a)

実行結果

[1, 1, 2]

実際には、以下の表のような処理が行われるので、上記のプログラムで assign_one(a) を呼び出すと、a の値は変化ます。

行数 行われる処理
4 a[0, 1, 2] が代入されます
5、1 assign_one が呼び出され、仮引数に対して x = a という代入処理が行われます。その結果、xa同じデータを共有 することになります
2 x[0] = 1 という代入処理が行われます。この処理では x に値を代入していない ので、xa のデータの共有は 解除されません。return 文を実行せずに、関数のブロックの処理が終了したので、関数の返り値は None になります
5 assign_one(a) が、関数の返り値の None に置き換わりますが、この行では代入処理は記述されていないので、置き換わった後で何の処理も行われません
6 ax同じデータを共有 しているので、2 行目の x[0] = 1 の処理によって a の値も変化します

まだピンとこない人がいるかもしれないので、上記のプログラムを、関数を使わない 同様の処理9 を行うプログラムに修正した下記のプログラムを見て下さい。下記のプログラムは「その 7」の記事で紹介した プログラム B と同様の処理を行っていますので、下記のプログラムを見てもピンとこない人は、「その 7」から復習して下さい。

a = [0, 1, 2]
x = a
x[0] = 1
print(a)

実行結果

[1, 1, 2]

関数呼び出しの代入処理と返り値

関数呼び出しを行うと、その部分が 関数の返り値に置き換わって 処理が行われるという説明をしました。その際に、返り値が return 文の 式の計算結果を複製したものであると 勘違い しないように 注意して下さい。返り値と return 文の式の計算結果は、同じデータを共有 しています。より具体的には、返り値を表すデータを管理するオブジェクトは、return 文の式の計算結果を管理するオブジェクトと 同じ です。

def 関数名(仮引数):
   関数のブロックのプログラム
   return 

a = 関数名(実引数)

上記のような、変数に 関数呼び出しを直接代入 するプログラムは、下記のプログラムと 同様の処理9 が行われます。

仮引数 = 実引数
関数のブロックのプログラム
a = 

このことと、「その 8」の記事以降で説明した Python で行われる代入処理 について 正しく理解 できていれば、「変数に関数呼び出しを直接代入する」処理を説明する際に、誤解されがちな例 としてよく取り上げられる下記の例で行われる処理を 正しく理解 できるはずです。

勘違いされがちな関数呼び出しの代入処理

下記のプログラムの 1、2 行目で定義した psuedo_copy という名前の関数は、仮引数 に渡されたデータを 複製したデータ を返り値として返すという 意図 で作られた関数です。ただし、実際にはこの関数で データの複製は行われません ので、この関数の名前は「偽の」という意味の英単語である psuedo と複製を表す copy を 組み合わせて psuedo_copy という名前にしました。

下記のプログラムを実行すると、以下のような処理が行われると 勘違い して、[1, 2, 3] が表示されると考える人がいるかもしれませんが、実際には [5, 2, 3] が表示されます。

下記は、間違った 処理の手順です

  1. psuedo_copy の返り値は、仮引数に代入された [1, 2, 3] を複製したものである(これが 間違い で、複製ではなく 共有 されます)
  2. 従って、b = psuedo_copy(a) によって b には a を複製したデータが代入される
1  def psuedo_copy(x):
2      return x
3
4  a = [1, 2, 3]
5  b = psuedo_copy(a)
6  a[0] = 5
7  print(b)
行番号のないプログラム
def psuedo_copy(x):
    return x

a = [1, 2, 3]
b = psuedo_copy(a)
a[0] = 5
print(b)

実行結果

[5, 2, 3]

実際には、以下の表のような処理が行われるので、上記のプログラムで assign_one(a) を呼び出した後で、a[0] = 5 を実行すると b の値も変化 します。

行数 行われる処理
4 a[1, 2, 3] が代入されます
5、1 psuedo_copy が呼び出され、仮引数に対して x = a という代入処理が行われます。その結果、xa同じデータを共有 することになります
2 return 文が記述されているので、return の後に記述されている x が関数の返り値(出力)となり、関数呼び出しの処理が終了します
5 psuedo_copy(a) が、関数の返り値の x に置き換わり、b = x が実行されます。xa同じデータを共有したまま なので、abx同じデータを共有 することになります
6 a[0] = 5 が実行されます
7 ab同じデータを共有 するので、b を表示すると a の値である [5, 2, 3] が表示されます

まだピンとこない人がいるかもしれないので、上記のプログラムを、関数を使わない 同様の処理9 を行うプログラムに修正した下記のプログラムを見て下さい。下記のプログラムは「その 7」の記事で紹介した プログラム B と同様の処理を行っていますので、下記のプログラムを見てもピンとこない人は、「その 7」から復習して下さい。

a = [1, 2, 3]
x = a
b = x
a[0] = 5
print(b)

実行結果

[5, 2, 3]

「その 7」の記事で、「変数の代入に関する処理を正しく理解することは、Python のプログラミングを行っていく上で非常に重要で、避けて通ることはできません」と説明したのは、このように 関数の処理正しく理解 する際にも 大きく関係 するからです。

関数呼び出しに関する注意点

仮引数と実引数の対応

今回の記事で紹介した関数の定義と関数呼び出しを記述する方法では、関数呼び出しの 実引数の数 と、関数の定義の 仮引数の数必ず一致する 必要があります。下記のプログラムのようにその数が一致しない場合は、関数呼び出しを実行すると エラーが発生 します。

下記のプログラムは、実引数の数が仮引数の数より 少ない 場合です。

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

print(sum(1))

実行結果

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[10], line 4
      1 def sum(x, y):
      2     return x + y
----> 4 print(sum(1))

TypeError: sum() missing 1 required positional argument: 'y'

上記のエラーメッセージは、以下のような意味を持ちます。

  • TypeError
    データ型(Type)に関するエラー
  • sum() missing 1 required positional argument: 'y'
    sum を呼び出す際に必要とされる(required)、場所が決まっている10 (positional)y の実引数(argument)が 1 つ存在しない(missing)

下記のプログラムは、実引数の数が仮引数の数より 多い 場合です。

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

print(sum(1, 2, 3))

実行結果

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[11], line 4
      1 def sum(x, y):
      2     return x + y
----> 4 print(sum(1, 2, 3))

TypeError: sum() takes 2 positional arguments but 3 were given

上記のエラーメッセージは、以下のような意味を持ちます。

  • TypeError
    データ型(Type)に関するエラー
  • sum() takes 2 positional arguments but 3 were given
    sum は 2 つの場所が決まっている(positional)実引数(argument)を受け取る(takes)が、3 つの実引数が与えられた(were given)

関数呼び出しを記述できる場所

Python では 関数の定義を実行する前 に、関数呼び出しを実行すると エラーが発生 します11

mul(1, 2)

def mul(x, y):
    return x * y

実行結果

---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[12], line 1
----> 1 mul(1, 2)
      3 def mul(x, y):
      4     return x * y

NameError: name 'mul' is not defined

上記のエラーメッセージは、以下のような意味を持ちます。

  • NameError
    名前(Name)に関するエラー
  • name 'mul' is not defined
    mul という名前(name)は定義されていない(is not defined)

入力、処理、出力が存在しない関数

最初に関数は 入力処理出力 の 3 つで構成されると説明しましたが、実際には入力、処理、出力が 存在しない関数を定義 する事が できます。それぞれについて説明します。

入力が存在しない関数

関数の入力は、入力したデータを使って 関数の処理を行う ために 利用される ものです。従って、入力データを必要としない処理 を行う関数を定義する際には、仮引数を記述しません。また、そのような関数を呼び出す場合に 実引数を記述しません。ただし、その場合でも関数の定義時と、関数呼び出しの際に、関数名の後に () を記述する必要がある 点に注意して下さい。

入力を必要としない関数の具体例としては、常に同じ処理を行う関数 が挙げられます。具体例としては、〇×ゲームのゲーム盤のデータを初期化する関数をこの後で紹介します。

また、現在時刻を表示する関数のように、常に同じ処理を行わない関数であっても、入力を必要としない場合があります。

入力がない場合でも、関数の定義と関数呼び出しの際に、必ず () を記述する必要がある

出力が存在しない関数

関数の出力を表す 返り値 は、関数の中で行われた処理の 結果を知らせる ためのデータですが、関数で行われた処理の結果を知らせる 必要がない 場合は、関数の定義のブロック内に return 文を記述しません。return 文が記述されていない関数を実行すると、データが存在しない ことを表す None が返り値 になります。

また、一般的にそのような関数の 返り値を利用 することは ない ので、そのような関数の 関数呼び出し式の中に記述する ことは通常は ありません

return 文 を記述しない関数のことを、返り値を返さない関数 と呼びます。以後は、返り値を返さない関数 の処理を説明する際に、None が返り値として返されるという 処理の説明を省略 します。

return 文が記述されていない関数の具体例としては、print が挙げられます。print は文字を表示するという処理を行いますが、関数の返り値という形でデータを表示しているわけでは ありません

下記のプログラムは、print の返り値を代入した a の値を画面に表示ていますが、2 行目で a を表示すると、データが存在しないということを表す None が画面に表示されます。

a = print(1)
print(a)

実行結果

1
None

return 文が記述されない関数のことを、返り値を返さない関数 と呼ぶ。

関数呼び出しを「誰かに何かを頼む」ことに例えると、関数の返り値は「頼んだ人からの 応答」に相当します。例えば、咳をしている子供に「体温計で熱を測って」という指示を行った場合は、「測った体温」を応答として受け取る必要があります。一方、「顔を洗ってきて」のように、自分が結果を知る必要がない ような指示に対しては、応答は必要がありません。

処理が存在しない関数

一般的には処理を必要としない関数を定義する事に意味はありませんが、処理を全く行わない関数を定義する事ができます。処理を全く行わない関数は、下記のプログラムのように、関数のブロックに pass のみ を記述します12

def a():
    pass

上記の関数は、入力、処理、出力が いずれも存在しない 関数です。このような関数を定義する意味はないと思うかもしれませんが、これから 実装する予定 の関数を 仮に定義だけ をしておき、後から 関数の処理を記述して 実装する などの目的で、実際に 上記のような関数を 定義する ことがあります。具体例については、必要になった時点で紹介します。

入力、出力という用語に関する補足

プログラムでは、入力出力 という 用語 は下記のように、様々な意味で使われる 点に 注意が必要 です。なお、下記は一例で、他にも入力、出力という用語が使われる場面は数多くあります。

  • 入力
    • 関数の実引数
    • キーボードから入力した文字
    • ファイルから読み込まれたデータ
    • カメラなどから得られた画像データ
    • マイクから得られた音
  • 出力
    • 関数の返り値
    • print によって表示される文字
    • ファイルに書き込まれるデータ
    • 画面に表示される画像
    • スピーカーに出力される音

例えば、これまで print は文字列を表示する処理を行う関数であると説明してきましたが、厳密には 標準出力 に文字列を 出力 するという処理を行います。標準出力への出力は、同じ出力という用語 ですが、関数の返り値による出力とは 全く異なる意味 を持ちます。

標準出力とは何かという説明は長くなるので省略しますが、JupyterLab の場合は、標準出力に出力された文字列が、実行したプログラムの すぐ下のセルに表示 されるようになっています。

実際に、print文字列を出力する関数 であるという説明を見かけることがあります。この説明は、一見すると関数の 返り値として文字列を出力 するという意味に 思えるかもしれません が、実際には print返り値を返さない関数 です。

このように、入力、出力という用語は、様々な意味で使われるため 紛らわしい場合がある 点に注意が必要です。本記事では、単に関数の入力出力 という用語を記述した場合は、実引数返り値 のことを表すことにします。また、それ以外の意味で入力、出力という用語を使う場合は、何に対するどのような入出力であるかがわかるように記述することにします。

名前空間

関数呼び出しで行われる処理を正しく理解するためには、名前空間 について理解する必要がありますが、今回の記事はかなり長くなりましたので、次回の記事で説明します。

関数のまとめ

今回の記事で行った関数に関する説明をまとめると以下のようになります。

  • 関数は、入力 されたデータに対して、何らかの 処理 を行い、処理の結果を 出力 するような機能を持つもののことである
  • 関数の定義を実行 する事で、プログラムでその関数を 利用することが出来るようになる
  • 定義した関数は、関数呼び出し を記述することで利用することができる
  • 関数に入力するデータの事を、実引数 と呼び、関数呼び出しの () の中に半角の , で区切って の形で記述する
  • 実引数を代入 して受け取る 変数 の事を、仮引数 と呼び、関数の定義の関数名の後の () の中に半角の , で区切って仮引数の 名前 を記述する
  • 関数が行う 処理 は、関数の定義の ブロックの中 に記述する
  • 関数呼び出しが実行されると、関数のブロックの処理を 行う前 に、仮引数に実引数を代入 するという処理が行われる
  • 関数の処理の結果として 出力 されるデータの事を、返り値 と呼ぶ。返り値は、関数のブロック内で return 文 を記述することで指定する
  • 関数呼び出しの処理が完了すると、関数呼び出しの部分が 返り値に置き換わって 処理が続行される

マスを配置する関数

関数の基本的な使い方が分かったので、(x, y) のマスにマークを配置する という処理を行う関数を定義して利用することにします。

関数を定義する際には 関数の名前入力処理出力決める 必要があります。この関数は、マスにマークを配置する処理 を行うので、配置 (place) とマーク (mark) を表す英単語を組み合わせて place_mark という名前の関数にすることにします。

この関数には、(x, y) の 2 つの座標 と、〇 か × を表す マーク を入力する必要があるので、仮引数は 3 つ必要になります。そこで、仮引数の名前xymark にすることにします。

この関数は、関数の中で行った処理の結果を返す必要がないので、返り値を必要としません。そのため、この関数は、ブロックの中に return 文を記述しない、返り値を返さない関数 として定義します。

下記は、前回の記事で作成した (0, 1) のマスに 〇 を配置するプログラムです。

if board[0][1] == " ":
    board[0][1] = ""
else:
    print("(0, 1) のマスにはマークが配置済です")

この関数を元に、(x, y) のマスに mark で指定したマークを配置する関数を記述すると、以下のようなプログラムになります。下記の修正箇所をクリックするか、上下のプログラムを直接見比べて、どことどこが対応しているかを確認して下さい。

def place_mark(x, y, mark):
    if board[x][y] == " ":
        board[x][y] = mark
    else:
        print("(", x, ",", y, ") のマスにはマークが配置済です")
修正箇所
+ def place_mark(x, y, mark):
- if board[0][1] == " ":
+    if board[x][y] == " ":
-    board[0][1] = ""
+        board[x][y] = mark
- else:
+    else:
-    print("(0, 1) のマスにはマークが配置済です")
+        print("(", x, ",", y, ") のマスにはマークが配置済です")

具体的な対応箇所は以下の表のようになります。

修正前 修正後
0 x
1 y
"〇" mark

ゲーム盤を初期化する関数

今後の実装で、ゲーム盤を初期化する処理頻繁に利用する ことが想定されるので、place_mark の動作確認を行う前に、ゲーム盤を初期化する処理を行う関数を定義することにします。

関数名は、初期化を表す initialize と盤を表す board を組み合わせて initialize_board にすることにします。ゲーム盤の初期化処理は、常に同じ処理 を行うので 仮引数は必要ありません。また、初期化の結果を返す必要がないので、返り値を必要としません

ゲーム盤の初期化を行う関数は下記のプログラムのように記述することができます。

def initialize_board():
    board = [[" "] * 3 for x in range(3)]

動作確認

必要な関数が定義できたので動作確認を行います。下記は前回のプログラムです。

board = [[" "] * 3 for x in range(3)]
if board[0][1] == " ":
    board[0][1] = ""
else:
    print("(0, 1) のマスにはマークが配置済です")
print(board)
if board[0][1] == " ":
    board[0][1] = "×"
else:
    print("(0, 1) のマスにはマークが配置済です")
print(board)

実行結果

[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
(0, 1) のマスにはマークが配置済です
[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]

上記のプログラムを、先ほど定義した関数を使って、下記のように記述して実行してみることにします。

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

実行結果を見ると、修正前と異なり、「( 0 , 1 ) のマスにはマークが配置済です」 が 2 回表示 されてしまいます。これは、修正後のプログラムのどこかに バグがある からです。

次回の記事では、修正後のプログラムのバグの原因を理解するために必要な知識として、名前空間とスコープについて説明します。

実行結果の (0, 1)( 0 , 1 ) が微妙に違うと思った人がいるかもしれません。その理由は、place_markprint で、, を使って 表示するデータを 並べて記述 しているからです。print, で区切ったデータを表示する際に、間に半角の空白を表示 する仕様になっています。

print を使って、空白を開けずに複数のデータを並べて表示する方法については、今後の記事で紹介します。

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

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

次回の記事

更新履歴

更新日時 更新内容
2023/09/29 for y in range(3)for x in range(3)
2023/09/26 「set_one」を「assign_one」に修正しました
「set_mark」を「place_mark」に修正しました
  1. 関数の定義の際に記述する def は、定義を表す define の略です

  2. Python では、変数名や関数名には、アルファベット、アラビア数字、_ という記号を使うことができます。ただし、アラビア数字を名前の先頭の文字として使用することはできません

  3. print で関数名を表示した際に表示される function は、関数を表す英単語です

  4. 関数名の後に () を記述しなかった場合は、関数呼び出しとは 異なる処理 が行われます。具体的にどのような処理が行われるかについては、そのような記述が必要になった時点で説明します

  5. 戻り値と呼ぶ場合もあります

  6. 過去の記事でも説明しましたが、1 のような単なる数値も 式の一種 です

  7. 用語が異なるのではじめはそのように思えないかもしれませんが、仮引数は、実引数の値で初期化されるという点を除けば、変数と全く同じ性質 を持ちます

  8. 「その 11」の記事では「疑似的な複製」と区別できるように「完全な複製」と表記していましたが、冗長なので今回の記事からは単に「複製」と記述します

  9. 先ほどのノートで説明したように、厳密には同じ処理ではありません 2 3

  10. 今回の記事では紹介していませんが、Python には記述する場所が決まっていない引数があります

  11. JavaScript など、関数を定義する前に関数呼び出しを行うことができるプログラム言語がありますが、Python ではそのようなことはできません

  12. 関数の定義でブロックに何も記述しないとエラーが発生します

0
0
5

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?