どくとる・めも

化学、数学、プログラミング、英語などに関する諸々

【Python】classの使用法【初心者向け】

はじめに

Pythonの勉強がある程度進んできたときに、登竜門的に立ちふさがる存在が"class"ではないかと個人的に考えている。実際、これを書いている最中も、未だにその正体をつかみきれていない。

どうも実際の開発で相手にするような大規模ソースコードだと活躍するらしいのだが、初学者が組むような短めのソースコードではあまりclassの恩恵を感じられないという発言も散見されるので、深く考えないことにした。もとよりこのブログは、私のメモなのだ。

なぜclassを使うのか?

端的に言えば、classを使うことで、異なるコードどうしであっても共通部分を使いまわすことができるようになり、コード間の差異の部分のみを必要に応じて埋めればすむ。というのが現時点での私の理解だ。

例えば、三人組で構成されるバンドを考えてみよう。各バンドメンバーには色々な差があるだろう。ある者はボーカルを担当し、ある者はギターを弾き、ある者は太鼓を叩く...という具合に(この編成だと低音部がスカスカになってしまうが)。
一方で、共通点も多い。彼らはみな人間で、みな音楽を好み、みなオリコン一位獲得を狙っている、といったところだろうか。

なにもバンドに限らず、異なる存在どうしがなにかしらの部分において共通しているというケースは多い。例えば、太郎と花子が国語・算数・英語の試験を受けたとしよう。両者の平均点を計算するプログラムを組みたい、というときに、どういうソースコードを書けるだろうか?

一番シンプルで素直な例は、以下のようなものだろう。

Taro_Japanese = 60
Taro_math = 90
Taro_English = 30

Hanako_Japanese = 40
Hanako_math = 80
Hanako_English = 90

print("The average score of Taro is {}".format((Taro_Japanese + Taro_math + Taro_English)/3))
print("The average score of Hanako is {}".format((Hanako_Japanese + Hanako_math + Hanako_English)/3))

はっきり言って馬鹿丸出しのコードだが、それでも平均点を求めるという目的は達成できている。
ところが太郎、花子のみならず二郎、三郎、四郎,,,n郎,,,という風に、大人数の試験結果をまとめる必要に駆られた場合はどうすればいいのだろうか。Jiro_Japanese, Jiro_mathといった具合に一々変数を用意していたのでは、とても身が持たない。結局のところ彼らは太郎、花子、二郎,,,と各々の氏名だけが異なるのであり、国語・算数・英語の試験を受けたというのは同じだ。だから、各々の名前だけは区別することにして、国語・算数・英語という科目に対しては、全員に共通のコードを割り当てた方が便利だ。

そこで、以下のような例でclassを作ってみる。例によって全体を俯瞰した後、個別に分割して解説する。

class TestScore:

    def __init__(self, Japanese, math, English):
        self.Japanese = Japanese
        self.math = math
        self.English = English

    def average(self):
        print((self.Japanese + self.math + self.English)/3)


Taro = TestScore(60, 90, 30)
Taro.average()

まずは、classを作る宣言をしよう。

class TestScore:

ここではTestScoreという名前のclassを作成した。def文やif文と同じように、名前のあとにコロン(:)を付して、続く行をインデントすればいい。

def __init__(self, Japanese, math, English):
        self.Japanese = Japanese
        self.math = math
        self.English = English

で、第二部分なのだが、いきなり見慣れないものがでてきた。__init__とはなんだろうか。selfとはなんだろうか。これを説明するために、少し順番が前後するが、ソースコード全体の後半部分に着目すると、以下のような記述がある。

Taro = TestScore(60, 90, 30)

この一文はどういうことだろうか?
今回扱っているclassというのは、料理のレシピに例えられる。例えば「カレーライス」というclassを考えるなら、人参を何g, 玉ねぎを何g, 米を何合用意した後に野菜を切って水を沸騰させて...という一連のプロセスが示されたものが「カレーライス」だ。
ところが、レシピだけあっても、肝心の材料が無いのではカレーにはありつけない。試験の平均点を求める場合なら、各科目の得点を合計して3で割ればいいということはわかっていても、各科目の得点がわからないと平均点は求まらない。だから(60, 90, 30)という得点(≒カレーの材料)を、TestScoreというclass(≒カレーを作るためのレシピ)に与えている、というのがこの一行だ。

そして、じつは(60, 90, 30)というのはそれぞれ国語、算数、英語の点数に対応している。なぜそう言えるのか? もう一度、第二部分を見てみよう。

def __init__(self, Japanese, math, English):
        self.Japanese = Japanese
        self.math = math
        self.English = English

def文だから、関数を定義しているのがわかる。そしてその後に(self, Japanese, math, English)と続いている。selfというのはなんだろうか?
今一度当初の目的を思い出すと、太郎と花子の試験の平均点を知りたいのだった。そのためにはただの「国語の得点」「算数の得点」ではなく、「太郎の国語の得点」「花子の算数の得点」という風に、その得点が誰に帰属されるのかを明確にしなければならない。その役割を担っているのがこのselfなのだ。
すなわちselfというのは、「今、誰の得点を考えているのか?」に応じて、その値が変わる。先ほどはTaro = TestScore(60, 90, 30)というコマンドを実行したため、self = Taroとなっている。しかし、Hanako = TestScore(40, 80, 90)というコマンドを実行すれば、このときはself = Hanakoとなるのだ。繰り返しになるが、「誰の得点なのか」を明確にしないといけないから、selfという変数を用いる必要がある。

そしてTestScore(60<第1変数>, 90<第2変数>, 30<第3変数>)というコマンドによって、Japanese<第1変数>, math<第2変数>, English<第3変数>に値が渡されている。だから第2部分は、(実際にはこんな書き方しないけど)、以下の様に読み替えることができる。

def __init__(Taro, 60, 90, 30):
        Taro.Japanese = 60
        Taro.math = 90
        Taro.English = 30

だいぶわかりやすくなったが、まだ__init__の部分がよくわからない。ここで、コード全体を以下の様に書き換えてみる。

class TestScore:

    def average(self):
        print((self.Japanese + self.math + self.English)/3)


Taro = TestScore()
Taro.Japanese = 60
Taro.math = 90
Taro.English = 30

Taro.average()

小難しい__init__の部分を省いてみた。実はこのコードでも得られる結果はまったく一緒なのだが、よく見ると、Taro = TestScore()という風に、class側に一切の変数を渡していない。だから後の方でTaro.Japanese = 60のように、各科目の点数を代入しているのだが、これは入力の手間がかかる。3科目くらいならなんとかなるだろうが、例えばこれが太郎の好きな楽曲100選とかになってしまうと手入力するのは無理だ。

やはり、さっきのようにTaro = TestScore(60, 90, 30)のように点数をinputしてやるほうが楽だろう。そのためには初期化メソッドである__init__が必要なのだ。もう一度初期化メソッドを見てみる。

def __init__(self, Japanese, math, English):
        self.Japanese = Japanese
        self.math = math
        self.English = English

このメソッドが、先ほどの__init__を使用しない例におけるTaro.Japanese = 60, Taro.math = 90, Taro.English = 30を代行してくれている。

残りの部分はそれほど珍しくない。2つめのdef文でaverageという関数を定義しているが、引数としてselfを用いるというのを忘れないようにしよう。そして定義した関数名はaverageなのだが、実際にこの関数を用いる場合には"Taro.average()"のように、書く必要がある。なんだかむずがゆい。

継承

ここまでに紹介した例は、正直ただのdef文でもなんとかなりそうなものだが、classを利用する大きな理由の一つに「継承」を使える、というものがある。
ここで、太郎でも花子でもない、留学生のRichardを登場させよう。彼は留学生なのだが、三科目に加えて面接も受けなけれないけない。では、Richard様にまた関数を作り直すべきか?たしかに面接の有無と言う差はあるが、三科目は共通してるから、太郎と花子ように作った関数に面接だけ付け足す、ということがしたい。それを可能にしてくれるのが「継承」であり、classを使うことにより得られる恩恵のひとつだ。

以下のようなコードを考えよう。

class TestScore:

    def __init__(self, Japanese, math, English):
        self.Japanese = Japanese
        self.math = math
        self.English = English

    def average(self):
        print((self.Japanese + self.math + self.English)/3)

class TestScorePlus(TestScore):
    def __init__(self, Japanese, math, English, interview):
        super().__init__(Japanese, math, English)
        self.interview = interview

    def averageplus(self):
        print((self.Japanese + self.math + self.English + self.interview)/4)

Richard = TestScorePlus(80, 70, 80, 90)
Richard.averageplus()

前半部分はさきほどと同じだから、2つめのclass文以降を見よう。

class TestScorePlus(TestScore):
    def __init__(self, Japanese, math, English, interview):
        super().__init__(Japanese, math, English)
        self.interview = interview

1行目はさきほどと違い、class class名1(class名2)という構造になっている。これにより、()内で指定したclassを継承できる
そして2行目は def __init__(self, Japanese, math, English, interview)となっている。Richardは面接(interview)を受けなければいけないから、その分だけ引数を増やしてやる必要がある。
3行目のsuper().__init__(Japanese, math, English)は、さっきのclass (TestScore)の設定を引き継いでいる。この一文は、以下の様に読み替えられる。

super().__init__(Japanese, math, English) #以下の3行と等価
  self.Japanese = Japanese
       self.math = math
       self.English = English

同じように、self.interviewの項目も必要だろう。だから4行目のように記述している。あとの議論は一緒だ。これにより、既存の関数を使いまわすことができるようになった。

終わりに

正直、このエントリはわかりにくい。なにしろ私自身がclassの真髄をよくわかっていない。だが、いろんなところでclassを含むスクリプトを目にするのも事実だ。だから、ひとまず現時点での粗い理解を、ここに書き留めておいた。知識がアップデートされた際には、このエントリを修正することにしよう。改善点等のご指摘、遠慮なくお申し付けください。

参考にしたサイトたち

www.atmarkit.co.jp
www.capa.co.jp
特に後者はわかりやすい。