Python言語の文法を、コードを書く流れに沿って説明していく連載。前回と今回は、クラスを取り上げている。今回は、そのうちの定義方法について説明する。これでPython言語の基礎文法は完了だ。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
ご注意:本記事は、@IT/Deep Insider編集部(デジタルアドバンテージ社)が「deepinsider.jp」というサイトから、内容を改変することなく、そのまま「@IT」へと転載したものです。このため用字用語の統一ルールなどは@ITのそれとは一致しません。あらかじめご了承ください。
前回は「クラスの利用方法」について紹介した。続けて今回は、「クラスの定義方法」について説明する。※脚注や図、コードリストの番号は前回からの続き番号としている。
本連載は、実際にライブラリ「TensorFlow」でディープラーニングのコードを書く流れに沿って、具体的にはLesson 1で掲載した図1-a/b/c/dのサンプルコードの順で、基礎文法が学んでいけるように目次を構成している。今回は、図1-c内の一部(【再掲】における赤枠内)に絞って、コードを取り上げる。
なお、本稿で示すサンプルコードの実行環境については、Lesson 1を一読してほしい。
Lesson 1でも示したように、本連載のすべてのサンプルコードは、下記のリンク先で実行もしくは参照できる。
「本連載『機械学習&ディープラーニング入門』シリーズレベルのPythonコードを書く」という観点では、クラスを利用することはあっても、クラスを定義することはない。というのも、ライブラリ『TensorFlow』を使って書くコードは短く、クラスを使うまでもないケースが多いからだ。
クラスを使う最大の利点を3つほど挙げるなら、以下のようになる(内容後述)。
コードが長く複雑になればなるほど、これらの利点がありがたくなる。
ただし、本連載のシリーズレベル、具体的には短いコードを上から順に実行するだけの場合、クラスのような構造は入れない方が、かえってコード全体の見通しがよくなり、理解しやすい。もしクラスを入れてしまうと、効率的になるどころか、逆にコードが長くなって、分かりづらくなってしまう。
ちなみに、より高度なレベル、例えばフルスクラッチ(=ライブラリやフレームワークを使わず、すべて完全に手書き)でディープラーニングのプログラムを記述するような場合は、コードが煩雑・複雑になるので、上記3つのクラスの利点が存分に生かせるようになる。よって、今日はざっと斜め読みして、より高度なレベルに進む前に、もう一度、ここの「クラスの定義」の説明を振り返る、という読み方でも構わない。
3つの利点をそれぞれ簡単に説明していこう。ここで説明するのは、プログラミング言語に共通する一般的な概念についてである。
1. カプセル化
1のカプセル化とは、雑然とした世界を「物体+機能」のまとまりごとにまとめて(=カプセルでくるんで)管理することである。例えば「電車が走る」「ミカンがなる」「ボーリングする」「バスが走る」など、さまざまな「物体+機能」が雑然と存在すると仮定する。これらを上から順にプログラムを書いていくと、何のまとまりもなく、コード全体の見通しは悪くなる(図16-1の左)。
そこで、例えば「電車が走る」と「バスが走る」の機能をひとまとめにして、「乗り物が走る」という機能にカプセル化すると、どうだろうか(図16-1の右)。漠然とした世界に構造が現れて、先ほどよりも扱いやすくなる。このような感じで、プログラミングする人が、扱いやすいまとまりをどんどんと作っていくこと。これこそがクラスを書くことの意義だ。
しかもカプセル化によって、プログラムの内部が概念別・機能別に分類・整理されていき、プログラム全体を把握しやすくなるメリットもある。また、例えば「電車が走る」という処理がプログラム上に何度も登場する場合、「電車が走る」のクラスを作成しておけば、それを再利用して何度も繰り返し実行できるというメリットもある(図16-2)。ちなみに、関数にも同様に再利用のメリットがあるので、同じ処理が2回以上ある場合は、関数もしくはクラスにまとめることをお勧めする。
2. 継承
次に2の継承とは、親クラス、子クラス、孫クラス、……のように子孫のクラスを作り、親から子へ、さらに子から孫へと、その祖先の機能がそのまま継承できるようにすることである。例えば、次のような親子継承の階層ツリーを考えてみよう。
電車とバスと車、いずれも「走る」という機能を有している。もちろん電車とバスと車という物体(=クラス)に、それぞれに「走る」機能を実装しても何ら問題ない。しかし「走る」は共通する機能なので、より上位の概念として「乗り物が走る」を共通的に実装して、電車とバスと車ではそれを継承して、何も実装しないようにする(図16-3)。そうした方が、重複するコードを書かなくてよくなるので、楽だし、機能の管理もたやすくなる(※厳密には、「走る」という動作にも、走行可能なスピードやら、エンジンの違いなどが存在するが、ここでは説明を簡単にするために考えないこととする)。
このように継承には、構造とコードをシンプルにできるメリットがある。しかし、継承が必要となる場面は、機能に重複が多数あったりするなど、かなり大きくて複雑なプログラムを書かなければならないときだ。大規模なアプリケーションの開発ではなく、シンプルで短いコードの実装作業が中心となる機械学習/ディープラーニングでは、継承の出番はほとんどないだろう。
3. アクセス制限
最後に3のアクセス制限とは、クラス内部だけで使われている変数やメソッドを、クラスの外部から見えなくする、隠蔽(いんぺい)する、言い方を換えると「アクセスを制限する」という機能である(図16-4)。ちなみにアクセス制限は、前述のカプセル化の一機能としても説明されることが多く、プログラミング言語によっては「アクセスできる度合い」を意味するアクセシビリティ(Accessibility)という別名でも呼ばれている。
では、クラス内部の一部の要素を隠蔽する理由は、何だろうか。これは、例えば複数の人でコードを書いている場合、誰がどこにアクセスしているかは、コードが長くなればなるほど把握できなくなっていくので、そういった「自分の管理できない範囲のアクセス」は構造的に排除してしまえば、把握や管理そのものが不要になる、という発想があるからだ。つまり、コードの把握と管理をシンプルにしよう、ということである。
Pythonのアクセス制限は、アンダースコア1個_もしくは2個__を変数やメソッドの名前の先頭に付けて表現する(ちなみに、他の言語の仕様ではprivateといったキーワードでメソッドを修飾してアクセシビリティを定義する場合が多いので、Pythonは独特である)。
アンダースコア1個_で始まる変数やメソッドは、「クラスの外部からアクセス(参照)はできるけど、あくまで慣習的ルールとしてアクセスしないようにしようね」という制限ルールを決めるためのものである。いわば紳士協定である。
一方、アンダースコア2個__はより強力で、実質的に外部からのアクセスが制限される。
このようにしてアクセス制限がかかった変数はプライベート変数、メソッドはプライベートメソッドと呼ぶ。
ちなみ、クラスだけでなく、Pythonファイル(.pyファイル)のモジュールでも同様に、変数や関数の名前をアンダースコア1個_やアンダースコア2個__で始めてアクセス制限のように表現すること(=アンダースコアのもう一つの使い方)はよくある。例えばタプルでデータを変数に代入する際に、使わないデータは_と単体で記述して「この変数は無視します」という意味を込めることが多い。例えばリスト15-1を、リスト15-2のように少し書き換えると、「_で置き換えられた変数x_testと変数y_testは、このコードでは使わないので無視します」という意味になる。
#import tensorflow as tf
#mnist = tf.keras.datasets.mnist
# 以下のコードを動かすためには、上記2行を事前に実行しておく必要がある
#---------------------------------------------------------------------
(x_train, y_train),(x_test, y_test) = mnist.load_data()
(x_train, y_train),(_, _) = mnist.load_data()
クラスを使う意義が分かったところで、実際にクラスを定義するコードを見てみよう。
前回のリスト14-1でSequentialクラスのインスタンスを生成する方法を説明した。今回は、このクラスの定義内容を、リスト15-3に示す(※元々のコードは長いので、筆者が必要最小限まで省略した)。
# ……省略……
# ※後述のリスト15-4を先に実行しないと、このコードはエラーになるので注意
class Sequential(Model):
# ……省略……
# コンストラクターの定義と実装
def __init__(self, layers=None, name=None):
# ……省略……
# インスタンス変数の利用(もしくは宣言と値の代入)
self.supports_masking = True
# ……省略……
# インスタンスメソッドの定義と実装
def add(self, layer):
# ……省略……
# インスタンス変数の利用(もしくは宣言と値の代入)
self.built = False
# ……省略……
# ……省略……
このコードはどう定義されているのか。図16-5を使って説明しよう。
まず、classキーワードで始まる文でクラスが定義されている。その文の丸括弧()内のModelは継承元のクラスである。ちなみに、継承元を指定しない場合は丸括弧()全体を省略してclass Sequential:とすればよい。
次に、def文でメソッド(=クラス版の関数)が定義されている。__init__という名称の場合、多くのプログラミング言語における「コンストラクター」と同等の機能を持つ特殊なメソッドになることは前回説明済みだ。
ここでdef文の引数であるselfに注目してほしい。これはインスタンスメソッドの定義文の第1引数に必ず記述する引数で、「自分自身(self)のオブジェクト」を意味する。インスタンスメソッドが呼び出されると、このselfには自分自身のオブジェクトが自動的にセットされる。例えば前回のリスト14-1
を見ると、指定している引数はリスト値のみで、selfは指定されていない。インスタンスメソッドの呼び出し時にはselfは指定しなくてよいのだ。def文によるメソッド内部では、このselfを通じて、つまりself.some_variableやself.some_method()のようなコードで、インスタンス変数を「利用」したり(もしくは「宣言と値の代入」をしたり)、インスタンスメソッドを呼び出したりできる。
次の引数にある=Noneの部分は、デフォルト引数である。Noneは、「なし」「空」を意味するPythonの特殊なオブジェクトである。他のプログラミング言語を知っている場合は、nullキーワードと同じものと説明すると分かりやすいだろう。
さて、前回のリスト14-1ではSequentialオブジェクト(=変数modelに代入されている)のインスタンスメソッドであるcompile()やfit()、evaluate()が呼び出されていた。しかしこれらは、Sequentialクラスには実装されていない。実は、継承元のModelクラスに共通的に実装されていて、そのクラスの機能が継承されているため、Sequentialクラスには再実装されていないのだ。
よって、そのModelクラスの定義内容も見てみよう(リスト15-4)(※これも、筆者が必要最小限まで省略した)。
from tensorflow.python.keras.engine.network import Network # 下記コードの実行に必要なモジュールを追記
# ……省略……
class Model(Network):
# ……省略……
def compile(self,
optimizer,
loss=None,
metrics=None,
loss_weights=None,
sample_weight_mode=None,
weighted_metrics=None,
target_tensors=None,
distribute=None,
**kwargs):
pass # ……省略……
def fit(self,
x=None,
y=None,
batch_size=None,
epochs=1,
verbose=1,
callbacks=None,
validation_split=0.,
validation_data=None,
shuffle=True,
class_weight=None,
sample_weight=None,
initial_epoch=0,
steps_per_epoch=None,
validation_steps=None,
max_queue_size=10,
workers=1,
use_multiprocessing=False,
**kwargs):
pass # ……省略……
def evaluate(self,
x=None,
y=None,
batch_size=None,
verbose=1,
sample_weight=None,
steps=None,
max_queue_size=10,
workers=1,
use_multiprocessing=False):
pass # ……省略……
# ……省略……
確かに、3つのメソッドが定義されている。selfやデフォルト引数など、使われている構文はすべて説明済みなので割愛する。
passは、「何もしない」ことを表す文で、関数やメソッド、クラス、条件分岐におけるブロックの中身を空にしたい場合に記述する。
最後に、本稿冒頭で示した図1-dにあるクラス定義の例も見ておこう。
class PrintDot(keras.callbacks.Callback):
def on_epoch_end(self, epoch, logs):
if epoch % 100 == 0: print('')
print('.', end='')
ここまでに説明してきた言語仕様が多く使われている。一つ一つしっかりと押さえてきた皆さんであれば、理解できるはずだ。
keras.callbacks.Callbackクラスを継承するPrintDotクラスをclass文で定義し、そのインスタンスメソッドとしてon_epoch_endをdef文で実装している。
コンストラクターは中身を何も実装する必要がなかったようで、省略されている。
本稿の説明では、if文の:の後で常に改行していたが、リスト15-5の例では改行していないことに違和感を持ったかもしれない。ブロックが1行しかない場合は、このように改行せずに「if文+ブロック」を1行で記述することも可能だ。このような書き方は推奨されてはいないが、見た目を良くするために使われることがあるので、知っておいてほしい。
残りのprint関数もこれまでに説明済みである。
クラスに関する有用な知識をもう一つだけ紹介しておきたい。
クラスのオブジェクトに対して、「対象のクラスからインスタンス化されたものかどうかを調べたい」というケースがある。そういった場合には、isinstance()関数が使える。リスト15-6はそのサンプルコードだ。コードの意味については、コード中のコメントを参考にしてほしい。
model = tf.keras.models.Sequential()
print(isinstance(model, tf.keras.models.Model))
# True(modelオブジェクトはtf.keras.models.Modelクラスを継承したtf.keras.models.Sequentialクラスのインスタンスである)
# ちなみに、データの型を比較/判別する場合は、基本的に次のように書く
a = 12.3
print(type(a) == float)
# True(aオブジェクトの型はfloatである)
print(type(model) == tf.keras.models.Model)
# False(modelオブジェクトの型はtf.keras.models.Modelクラスではない)
print(type(model) == tf.keras.models.Sequential)
# True(modelオブジェクトの型はtf.keras.models.Sequentialクラスである)
次回からは、応用的だがよく使われる文法を、落穂拾いとして簡単に紹介していく。
Copyright© Digital Advantage Corp. All Rights Reserved.