Lesson 8 関数の定義 ― Python基礎文法入門:機械学習&ディープラーニング入門(Python編)
Python言語の文法を、コードを書く流れに沿って説明していく連載。前回に続けて、今回は関数の定義方法を説明する。加えて、デフォルト引数やキーワード引数という重要機能、Python言語で特徴的なインデントについても説明する。
ご注意:本記事は、@IT/Deep Insider編集部(デジタルアドバンテージ社)が「deepinsider.jp」というサイトから、内容を改変することなく、そのまま「@IT」へと転載したものです。このため用字用語の統一ルールなどは@ITのそれとは一致しません。あらかじめご了承ください。
前回は「関数」の利用方法について紹介した。今回は、定義方法を説明する。※脚注や図、コードリストの番号は前回からの続き番号としている。
本連載は、実際にライブラリ「TensorFlow」でディープラーニングのコードを書く流れに沿って、具体的にはLesson 1で掲載した図1-a/b/c/dのサンプルコードの順で、基礎文法が学んでいけるように目次を構成している。関数の定義は3つほど含まれているが、すべてを詳しく説明する必要はないだろう。そこで本稿では、前回Lesson 7で説明した図1-aの初出の「関数を使用するコード」、具体的には
mnist.load_data()
という関数が、どのように定義されているかを見てみる。本稿の最後で、図1-b/dに含まれている「関数の定義例」も示す。
なお、本稿で示すサンプルコードの実行環境については、Lesson 1を一読してほしい。
Lesson 1でも示したように、本連載のすべてのサンプルコードは、下記のリンク先で実行もしくは参照できる。
Python言語の基礎文法
前回のLesson 7では「キーワード引数」という用語が何度も登場した。この言語機能は、「関数の定義」における「デフォルト引数」という仕様とも関係しているので、両方の言語機能をセットで理解する必要がある。そこで今回は、まず関数の定義方法を示し、その後でこの2つの言語機能を説明することとする。
関数の定義
それでは、Lesson 7で「関数の利用方法」として説明したload_data()関数が、どのように定義されているのかを見てみよう。
ライブラリ「TensorFlow」はオープンソースなので、インターネット上で該当箇所のソースコードを簡単に参照できる。例えばload_data()関数は、図12-1のようになっている(※2018年10月時点。TensorFlowは頻繁にアップデートされているので、コード内容は随時変わる可能性がある)。
図12-1は少し長いので、一部を省略して短くしたリスト11-1も掲載しておこう。
def load_data(path='mnist.npz'):
# ……内部で何らかの処理を実行……
x_train, y_train = ['x_train'], ['y_train']
x_test, y_test = ['x_test'], ['y_test']
return (x_train, y_train), (x_test, y_test)
ポイントは、図12-1で赤色の枠が書かれているところだ。defキーワードで始まる文で関数を定義し、returnキーワードで始まる文で戻り値を返している。図12-2は、関数定義のポイントと、引数と戻り値の関係を表現したイメージだ(※前回の図11-2とほぼ同じ絵だが、return ……の文を追記している)。ただし、return文がない関数もある。そうした関数は画面にドットを表示するなど、処理結果を戻り値として返す代わりに別の形で処理結果を表すために使われる。
def文は、以下の構文になっている。
- 構文: def <関数名>(<引数は0個〜カンマ区切りで複数>):
- コード例: def load_data(path='mnist.npz'):
インデントで明示するブロック/スコープ
まずはdef文の最後にコロン:が付けられている点に注目してほしい。このコロンは、次行以降のインデント(後述)された複数行が関数のコードであることを明示するためのものだ。
インデントとは、半角スペースで左に余白を作ることで、通常は4つの半角スペースで作る。半角スペースの数は、インデント幅と呼ばれる。
ちなみに、Lesson 3でも説明したように、半角スペースの有無のようなルールはコーディング規約もしくはスタイルガイドと呼ばれる。スペース2つがルールになっているケースもあり、例えば本連載が対象としているTensorFlowのスタイルガイド「TensorFlow Style Guide」では、インデントは半角スペース2つが推奨されている。
本連載では、横幅節約のため、2つの半角スペースにしている。ちなみに、Google Colabのインデント幅のデフォルト設定も、同じ半角スペース2つである(※4つにしたい場合は、メニューバーの[ツール]−[設定]から開く[設定]ダイアログの[インデント幅(スペース)]欄の数値を2から変えることで変更できる)。
Google Colabでインデントを作成するには、実際に[スペース]キーを2回押す方法だけでなく、行の先頭など行内で[Tab]キーを押してもよい(※後述の【注意】を参照)。
逆にインデントを解除するには、[Shift]+[Tab]キーを押すとよい。
ただし、Lesson 3で[Tab]キーのショートカットがあることを示したとおり、
- 入力中の単語の最後で[Tab]キーを押すと(インデントではなく)オートコンプリート
- 関数の(〜)の間で[Tab]キーを押すと、ヘルプドキュメント(docstringヘルプ)
が表示される。このように[Tab]キーを押す場所で挙動が変わることに注意してほしい。
【注意】[Tab]キーで入力される文字について
Google Colabでは、[Tab]キーを押すと、キーが表すタブ文字ではなく、半角スペース2つ(デフォルト設定)が入力される点に注意が必要だ。[Tab]キーの挙動がこのようになっており、逆に言うとタブ文字を入力するのは難しい。
もちろんインデントに(半角スペースではなく)タブ文字を使うことも可能ではある。しかし、そもそもタブ文字は、環境によってインデント幅の見た目が異なる可能性があり、問題がある。よって極力、タブ文字は使わない方がいい。
エディターによっては、[Tab]キーで(半角スペースによるインデントではなく)タブ文字が入力されてしまう。そのため、コード内でタブ文字と複数スペース(2つか4つ)が混在してしまう可能性がある(※混在するとPython 3ではエラーになってしまう)。そうならないためにも、[Tab]キーを押す方法ではなく、実際に[スペース]キーを2回押す方法で、インデント入力の癖を付けておく方が無難かもしれない。ちなみに筆者は、[スペース]キーを2回押している(※多くのコードエディターでは、一度、インデントを作ると、その後は改行のたびに同じインデント幅を維持してくれる。つまり、[スペース]キーを2回押すからといって、作業効率はそこまで悪くならない)。
Pythonは、インデントによって関数の始まり〜終りの範囲(プログラミング用語でブロックと呼び、関数のブロックはスコープとも呼ばれる)を定義するという特徴がある(図12-3)。インデント幅は、このブロックごと統一する必要がある。
なお、インデントされた複数行の途中に空行があったとしても、それ以降でインデントがそろっている限りは、「ブロック(ここでは関数のスコープ)は終了」と見なされない。よって、複数行の各行はすき間なく詰める必要はなく、見やすいように、適宜、改行で前後に余白を入れながら、ブロックのコードを書いていくことが可能だ(図12-4)。
ちなみに、インデントを解除しなくても、コードセルやPython(.py)ファイルの末尾に到達すると、当然、ブロック(ここでは関数のスコープ)もそこで終了する(図12-4)。
このインデントの仕様は、次回Lesson 9以降の「制御フロー文」や「クラス」でもまったく同じなので、ここで確実に押さえてほしい。さらに、「制御フロー文」の「ブロックとスコープの違い」の節で、ブロックとスコープの違いについてより詳しく説明するので、そちらも後で確認してほしい。
デフォルト引数
再び、図12-1および図12-2の話に戻すと、load_data()関数の引数はpathである。ここで「おかしい」と気付いたかもしれない。先ほどの図10-3で、この関数は「引数なし」として説明した。しかし実際の定義には、引数pathが存在している。つまり、冒頭でも示した関数の使用例(リスト11-2)では、引数の指定を省略していたのである。
#import tensorflow as tf
#mnist = tf.keras.datasets.mnist
# 以下のコードを動かすためには、上記2行を事前に実行しておく必要がある
#---------------------------------------------------------------------
mnist.load_data()
引数を省略せずに指定するには、リスト11-3のように記述すればよい。
mnist.load_data('mnist.npz')
なぜ引数を省略できるかというと、前掲のリスト11-1を見ると分かるように、
def load_data(path='mnist.npz'):
という関数の定義になっているからだ。つまり関数定義の段階で、引数pathに'mnist.npz'*5という文字列値がデフォルトで代入されているためである。
*5 'mnist.npz'は、TensorFlowが提供するファイルであり、言語仕様とは関係がないので説明を割愛する。
このように、デフォルト値を持った引数のことをデフォルト引数と呼ぶ。デフォルト引数があれば、その引数の指定は省略して呼び出せるのである。
ちなみに復習として、関数の定義でデフォルト引数ではなく通常の引数にする場合は、
def load_data(path):
と、=以降のデフォルト値部分を記載しないようにすればよい。
デフォルト引数は、引数の数が多いが、通常は決まった値を渡せばよいというときに特に役立つ。例えば関数が10個の引数を持つとしよう(リスト11-3)。
def some_function(param1, param2=2, param3=3, param4=4, param5=5, param6=6, param7=7, param8=8, param9=9, param10=10):
return param1 + param2 + param3 + param4 + param5 +param6 + param7 +param8 + param9 + param10
※=の前後には半角スペースを入れてもよい。デフォルト引数の指定では入れないことが一般的。
この関数を呼び出す場合、通常であれば、リスト11-4のように、すべての引数を順番に指定しなければならない。
some_function(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
# 55と出力される
しかし、この関数は第2引数以降がすべてデフォルト引数である。各引数に渡す値がデフォルト引数と全く同じ値(=第2引数以降に2、3、4、5、6、7、8、9、10を順に渡す場合)であるなら、そのすべてが省略可能だ。実際に、第1引数のみ指定して、残りを省略すると、リスト11-5のようになる。
some_function(1)
# 55と出力される
キーワード引数
各引数に渡す値がそのデフォルト引数と同じであれば、その引数は省略できることが分かった。しかし例えば第9引数のみ、デフォルト値ではない値を指定したい場合、リスト11-6のように、第8引数まで全部指定しなければならないのだろうか。
some_function(1, 2, 3, 4, 5, 6, 7, 8, 999)
# 1045と出力される
ここで役立つのがキーワード引数である。キーワード引数とは、関数の呼び出し時に引数名(=パラメーター名)を明示的に指定することである。例えばリスト11-7は、キーワード引数を使って、関数の第9引数に値を渡している例である。
some_function(1, param9=999)
# 1045と出力される
※=の前後には半角スペースを入れてもよい。キーワード引数の指定では入れないことが一般的。
キーワード引数は、関数定義にある引数名を使って、
<引数名>=<データや値>
という構文で引数に値を指定して、関数を呼び出す。リスト11-7の例では、
some_function(<整数値>, param9=<整数値>)
というコードで、まずは順番どおりの指定で第1引数に整数値を、次にキーワード引数で第9引数param9に整数値を渡しながら、関数を呼び出している。その2つ以外の引数(すべてデフォルト引数)はデフォルト値のままでよいので、引数を省略している。なお、リスト11-7の関数呼び出しにおいて、第1引数はキーワードを指定していないが、このようにキーワード指定をせずに、関数定義で定められた順番で渡される引数のことを「位置引数」(positional argument)と呼ぶ。ただし、「some_function(param9=999, param1=1)」のような書き方も可能だ(この場合はparam1もキーワード引数となる)。
複数の引数に対してデフォルト引数と同じ値が設定されている場合には、リスト11-6のように順番どおりにすべての引数に値を指定するのは、コードがムダに長くなって読みづらくなるし、そもそも面倒である。リスト11-7のように、使う引数だけを、個別に指定して値を渡した方が効率的である。もう一つだけ例を示そう。
図12-5は、後述する図1-bにあるキーワード引数の活用例である。plt.imshow()関数のヘルプが表示されており、X, cmap=None, norm=None, aspect=None, interpolation=None, alpha=None, vmin=None, vmax=None, origin=None, extent=None, shape=None, filternorm=1, filterrad=4.0, imlim=None, resample=None, url=None, *, data=None, **kwargsと多数の引数が用意されていることが分かる。
これらすべての引数を逐一、指定するのが面倒であることは容易に想像できるだろう。実際に図12-5の関数呼び出しでは、第1引数Xと、キーワード引数cmap(前回説明)の2つしか指定しておらず、かなりシンプルな呼び出しになっているのが分かる。
なお、キーワード引数の指定順序は自由にカスタマイズできる。ということは、「順番は気にせずに、[Tab]キーでヘルプドキュメントを見ながら、必要な引数から好きに書いていけばよい」ということである。例えば引数が10個も20個もある場合には、意外に重宝する機能である。
さまざまな関数の定義例
以上、関数の定義方法の基本について解説した。
関数の定義の解説としては、ここで終わりにしてもよいが、図1-b/d内にはいくつかの関数定義のコードが含まれている。そこで以下では、「定義例」という形でコードを提示し、必要最小限で説明していくことにする。コードの掲載は長くなるが、同じことの繰り返しなので、軽く読み流すだけで十分である。
おまけとして、関数の処理内容・意味をコード内のコメントとして追記している。ただし、処理内容や意味を理解するには、ディープラーニングおよびニューラルネットワークの理解がある程度必要となるので、注意してほしい。よく分からない場合は、本稿では意味理解を追求せず、「そのような意味のコードらしい」という程度でスルーしてほしい。
以下で紹介するのは、図1-b/d【再掲】における赤枠内のコードのみとなる。各関数定義の中身については、別の回に説明済み、もしくは説明予定なので、記載を省略する。
図1-bに含まれる「関数定義」のコード
図1-bに含まれている「関数」のコードは、リスト11-8のようになっている。
# 画像をプロット(=描画)する関数
def plot_image(i, predictions_array, true_label, img):
# 予測結果と正解と画像を、インデックス番号(i)を指定してNumPy配列データから1つ取得
predictions_array, true_label, img = predictions_array[i], true_label[i], img[i]
# ……何らかの処理(一部は前回説明)……
図1-dに含まれる「関数定義」のコード
図1-dの「関数定義」は、リスト11-19に示す。これも難しくはないだろう。これはreturn文を持たない関数の例といえる。
# ドット(.)を出力するクラスの定義
#class PrintDot(keras.callbacks.Callback): # Lesson 12で説明
# 各エポックの最後に呼び出されるクラスのメソッド(=関数の一種)の定義
def on_epoch_end(self, epoch, logs):
# ……何らかの処理(前回説明)……
print('.', end='')
on_epoch_end()関数の上位階層にclass PrintDotと記載されている。つまりこれはクラスの関数(正確には「メソッド」と呼ぶ)を定義している例である。
つづく
以上、「関数の定義方法」を説明した。次回は、「制御構文(条件分岐)」を説明する。プログラムの流れを、条件によって分岐させるための大事な文法である。
Copyright© Digital Advantage Corp. All Rights Reserved.