初心者がPythonのクラスについて理解しようとする話

f:id:shirakonotempura:20190120050031j:plain

はじめに

これまで、私が公開したいくつかのコードでは、写経部分ではたまに使っているかもしれませんが、自分のコーディング部分にはこれでもかというくらいにclassを使っていません.

というのは、全然使い方が理解できていないからです.それに関数defがあれば十分なのではと考えていました.

しかし、例えばKerasのドキュメントなんかにいくと

ImageDataGeneratorクラス

とか当然のようにclassが使われています.チュートリアルやサンプルが充実しているので、見よう見まねで使えてしまうのですが、少しは理解しておかないと痛い目見るだろうと思い、ようやく調べてみることにしました.


注記)記事中に、classとクラスが混在してしていますが、基本的に同じ意味です.ややこしくて申し訳ありません.

classに対するイメージ

現時点で、私がclassに対して持っているイメージです.

  • 複雑そう.
  • 使えたら便利そうだけど、defでがんばれば、使えなくても大丈夫そう
  • よくselfってみるけどなんや・・.

こんなレベルですので、コーディングの前に少し言葉の意味などを調べるところから始めてみます.

class(クラス)とは

まず、classっていったい何なのか、その定義を調べようとしたのですが、 - classがあれば同じコードを何度も書く必要がなくなります.

とか、 - classを理解するには、オブジェクト指向をまず理解・・.

のようになります.定義を調べたいのですが、コード中のclassの定義の仕方や、使い方の説明ばかり出てきます.

こんな質問まで見つかりました(笑)

Python - クラスって存在する意味あるんですか?|teratail

おそらく、classそのものの意味を定義することは難しくて、なるほどこういったことをできる機能をclassというのだと後で理解できるように なるのだと期待しておきます.

ですので一旦、定義のことは忘れてclassで何ができるかを調べていきたいと思います.


なお、私は当然のようにオブジェクト指向を理解していませんでしたので、それについても少し調べました.

こちらもclassに負けず劣らず理解の難しい得たいの知れないやつなんですが、オブジェクト指向に関しては全く分からない方にとっては以下の記事が参考になるかと思います.あくまで概念の話ではあるのですが、一番分かりやすかったです.こういう記事を書けるようになりたいです.

satoshi.blogs.com

classでできること

classについてのこちらの記事では、のように書いています.

classを使用すると、関数を定義するときのように複数の処理や値をまとめておくことができる.

classからは、インスタンスというものが作成でき、インスタンスにはデータを保持させておくことができたり(属性)、関数のように処理をまとめておくことができたり(メソッド)します.このように『1つのもの(オブジェクト)に情報や処理をひも付けて管理できる』のがclassです.

出典:具体例で学ぶ、Pythonのclassの使い方

見慣れない言葉がたくさん出てきて心が折れそうになりますが、私なりの理解も加えながら1つずつ見ていきます.

  • インスタンス:クラス(設計図)をもとに具体化したもの.量産される何か
  • 属性インスタンスごとに持つ個別のデータ.
  • メソッド:クラスの中で呼ぶ関数のこと.のちほど出てくるコンストラクinit() もメソッドの一つ.defで定義する関数はクラスの中ではメソッドと呼ばれているんだな、と捉えて多分大丈夫です.
  • オブジェクト: 数値、文字列、関数などすべてを指す.これもあまり深く考えなくてよさそう.というか、ガチ勢でなければたぶん深く考えてはいけないオブジェクト指向と呼ばれるプログラム言語で扱うモノは全てオブジェクトくらいの勢いでOK.


なんとなく掴めてきたイメージは、クラスっていうベースになる設計図があって、それがあればベースの機能(関数とか)が使えるインスタンスと呼ばれるオブジェクトが量産できるってこと.でも、まだdefで定義する関数との違いが良く分かりません.

関数を集めたファイル:モジュール

いわゆる関数だけを集めたまとまりとしてモジュールがあります.モジュールというのは、tools.pyとかのように名前が付けられたPythonファイルのことで、通常そのファイルの中にはたくさんの関数が書かれます.(ファイルから読み込む機能のことをモジュール機能と言うっていう解釈の方が正しいかもしれません)

例えば、tools.pyファイルの中に、runrun, hithitという名前の関数を定義している場合、以下のようにすれば、関数runrunおよび関数hithitを使用することができるようになります.

from tools import runrun
from tools import hithit

複数行書くのが面倒な場合、from tools import *でも取り込めますが、この場合ファイル内の全ての関数を取り込んでしまいますので、余りスマートではありません.

ライブラリやパッケージとは

モジュールと同じように、複数の関数を使ったりするライブラリやパッケージというものがあります.これらもモジュールと同じように使えます.

詳しく理解できていないので、あくまでイメージとして捉えてほしいですが、乱暴に書けば、モジュールが複数の関数が書かれたファイルだとすると、ライブラリは複数のモジュールが集まったものパッケージは複数のライブラリおよびモジュールが集まったものです.

さらに乱暴に言ってしまえば

パッケージ >ライブラリ > モジュール.でも使い方は同じ

というイメージで問題ないかと思います.

クラスの呼び出し方

話をクラスに戻しますが、クラスもモジュールと同じような書き方で使うことができます.クラス

例えば、my_school.pyというファイルの中に、Studentというクラスを持っているとします.同じファイルの中に他のクラスが入っていても問題ないです.また、クラスを別ファイルで作成しなければいけないわけでもありません.

from my_school import Student

呼び出し方に違いがないことが分かってどうするって思いますよね.私もです.

具体的にコーディングしてみる

結局、ここまでしてきた話では(少なくとも私は)、やはりまだクラスの恩恵を感じていません. こうなったら恩恵が得られることを信じて、具体的にクラスを使ったコードを書いていきたいと思います.

以下のような設定となるclassを作っていきます.

例1 class Student

  • studentクラスから作られる個別の学生インスタンスは、属性として名前、年齢、身長、体重を持つ.
  • 自己紹介を行うメソッドと、身長と体重からBMIとベスト体重を計算して発表するメソッドを持たせる.

classの定義:

classは、関数とほぼ関数と同じように書きます.ここでは、Studentというクラスを作成します. クラス名の最初の1文字目は大文字とするのが慣例のよう.2単語以上つなげる場合は、単語ごとに最初の文字を大文字にしましょう.

class Student:
    pass # ただ定義するだけなので、このクラスは何の機能もありません.

これで、学生インスタンスを作り出す設計図(量産工場)ができました.

インスタンスの作成:

では、各学生に相当する具体的なインスタンス(オブジェクト)を作ってみます. 以下のように、作成したclass(ここではStudent)を変数に代入すれば作成できます.

student1 = Student()

ここのstudent1というのが、各プレイヤーごとに与えるインスタンス名になります。これでstudent1という中身が空っぽの学生インスタンスが現れたことになります.

属性を与える:

学生にの名前・身長・体重という情報を、属性として与えます.student1に与える場合、次のように書けます.

student1.name = "matsuzaka"
student1.height = 183
student1.weight = 93

同様に以下のようにすればstudent2を作成できます.

student2 = Student()
player2.name     = "mori"
player2.height   = 170
player2.weight   = 80

まだ、恩恵は何も感じていないと思いますが、もう少しお付き合いください.

class内の関数であるメソッドを使う

上の例では、nameやheightを一行ずつ与えていますが、少し面倒ですしスマートではありません.class内の関数であるメソッドを使えば、もう少し省略することができます.

メソッドは関数と同様にdef(): という形式で書くことができますが、第1引数にselfというオブジェクトを指定する必要があります.

selfもなかなか理解が複雑な曲者なんですが、self:自身という名前が示すとおり、メソッドを呼び出したインスタンス自身のことを示します.具体的なイメージを持つため、実際にselfを使って、classにメソッドを追加してみます.

  • set_data:nameやheightを設定するメソッド
  • say_my_data:自己紹介するメソッド
  • my_BMI:自分のBMIと適正体重を言うメソッド
# メソッドを使用したクラス
class Student():
  def set_data(self, name, height, weight)
    self.name = name
    self.height = height
    self.weight = weight
  
  # 自己紹介するメソッド
  def say_my_data(self):
    print(print("I am {}.
                My height is {}cm.
                My weight is {}kg."
         .format(self.name, self.height, self.weight)))


  # BMIと適正体重を言うメソッド
  def my_BMI(self):
    bmi = round(self.weight /(self.height/100.)**2)
    best_weight = round((self.height/100.)**2 * 22)
    print("My BMI is {}.
                  My BEST weight is {} kg."
         .format(bmi, best_weight))

メソッドを実行する(引数を与える)側ではselfは引数には指定してはいけません.メソッド側の第2引数から指定してください.

  • メソッドを使えるように書き換えた後
student1 = Student() # インスタンスを作成
student1.set_data("matsuzaka", 183, 93) # 属性を一気に設定
  • 参考:メソッドを使う前はこれ(再掲):
student1 = Player()
student1.name = "matsuzaka"
student1.height = 183
student1.weight = 93

以下を実行すると、

player1.say_my_data()
player1.my_BMI()

以下のように出力されます

I am matsuzaka. My height is 183cm. My weight is 93 kg.
My BMI is 19. My BEST weight is 79 kg.

インスタンスを作成する際に呼び出すメソッド:コンストラク

これで、学生の量産が比較的簡単にできるようになりました.ですが、量産部分のコードを見て思われるかもしれませんが、インスタンスを設定→属性を設定という作業が地味に面倒です.

そこで、コンストラクというものが登場します.コンストラクインスタンス作成と同時に自動的に呼び出されるメソッドのことを指します.

コンストラクタを使うことで、先の例でset_dataメソッドを呼んでいたstudent1.set_data(引数)の一行を省略することが可能になります.

コンストラク__ini__()を使う

コンストラクタは、メソッド名が決まっており__init__()と定義する必要があります.

# コンストラクタを使ったPlayerクラスの定義
class Student:
  def __init__(self, name, height, weight):
    self.name = name
    self.height = height
    self.weight = weight

これで、大分スマートにインスタンス(学生オブジェクト)を量産することができます.今は属性の情報が非常に単純なんでこれが一体何になるのかいまいちピンとこないかもしれません.

例えば、対戦ゲームなんかで、取れる行動の種類などは同じだけど、置かれている状況は違う.といった場合なんかに、それぞれのインタンスの管理がしやすいんじゃないかと思います.たぶんマルチエージェントになるとクラスを使わないと無理なんじゃないかと思えてきました.

すみません.語彙力がなさすぎてうまく説明できない・・・.

クラスの継承

気を撮り直して説明を続けます.classの特徴の1つがここで説明する継承機能と呼ばれるものです.

この機能を利用することで、既に作成したclassをベースに、機能の追加を容易に行うことができます.

先ほど作成したクラス(Student)をベースにして、Universityクラスを作ってみます.

classの継承は以下のように、新しく定義するclass名に続く( )内に、継承したいclass名を指定すればOKです.

class University(Student):
    pass

もし、上のようにその他のコードを何も触らなかった場合、UnivesityクラスとStudentoクラスは全く同じになります.

student1 = University("matsuzaka", 183,  93)
  • メソッドを実行
student1.say_my_data()
student1.my_BMI()
  • これまでと全く同じ出力をするだけです.

    I'm matsuzaka. My height is 183cm. My weight is 93 kg.
    My BMI is 28. My BEST weight is 74 kg.

Universityクラスのコンストラクタを拡張し、大学名と出身県を紹介するセリフを追加してみます.

class University(Student):
  def __init__(self, name, height, weight, univ_name, pref):
    super().__init__(name, height, weight)
    self.univ_name = univ_name
    self.pref = pref
  
  def say_my_univ(self):
    print("I study at {}, I'm from {} prefucture"
          .format(self.univ_name, self.pref))

インスタントstudent3を作成します.コンストラクタの引数を増やしたので、大学名と出身県を追加しています.

student3 = University("uchikawa", 185, 93, "Fukuoka Univerisity", "Ohita")

say_my_dataメソッド、my_BMIメソッドは継承されているので、これまでと同じように呼び出すことができます.

student3.say_my_data()
student3.my_BMI()
student3.say_my_univ()

この場合、出力は以下のようになります.

I'm uchikawa. My height is 185cm. My weight is 93 kg.
My BMI is 27. My BEST weight is 75 kg.
I study at Fukuoka Univerisity, I'm from Ohita prefucture

継承の継承もできる

なお、以下のように継承したクラスの継承も可能です.

class Doctor(University):
  def __init__(self, name, height, weight, univ_name, pref, study):
    super().__init__(name, height, weight, univ_name, pref)
    self.study = study
    
  def my_study(self):
    print("I study about {} at {} as posdoc"
         .format(self.study, self.univ_name))

以下を実行すると

student4 = Doctor("yamada", 180, 76, "Osaka Univerisity", "Hyogo", "Yakult")
student4.say_my_data()
student4.my_BMI()
student4.say_my_univ()
student4.my_study()

こう出力されます.

I'm yamada. My height is 180cm. My weight is 76 kg.
My BMI is 23. My BEST weight is 71 kg.
I study at Osaka Univerisity, I'm from Hyogo prefecture.
I study about Yakult at Osaka Univerisity as posdoc

例2 class Countman

ただ、指定した範囲の偶数と奇数をリスト化するだけのクラスを作ってみます.

  • Countman:コンストラクタは開始値と終了値
  • count_even:指定範囲にある偶数のみをリスト化するメソッド
  • count_odd:指定範囲にある奇数のみをリスト化するメソッド
class Countman():
  def __init__(self,start, finish):
    self.start = start
    self.finish = finish
    
  def count_even(self):
    list = []
    for i in range(self.start, self.finish+1):
      if i % 2 == 0:
        list.append(i)
    return list

  def count_odd(self):
    list = []
    for i in range(self.start, self.finish+1):
      if i % 2 == 1:
        list.append(i)
    return list

インスタンスを作成

指定範囲の異なる2つのインスタンスを作成

countman1 = Countman(20, 30)
countman2 = Countman(1220, 1230)

結果の確認

それぞれのインタンスに対して、メソッドを実施

print(countman1.count_even())
print(countman1.count_odd())
print(countman2.count_even())
print(countman2.count_odd())

出力は以下.大丈夫そうです.

[20, 22, 24, 26, 28, 30]
[21, 23, 25, 27, 29]
[1220, 1222, 1224, 1226, 1228, 1230]
[1221, 1223, 1225, 1227, 1229]

まとめ

クラスについて、少し勉強しました.

今回書いたコード程度では、その恩恵を感じられたわけではないのですが、なんか便利そうだということは少し理解できました.

例えば、マルチエージェントのように同じような操作を並列に行う場合などの管理には必須と言えそうです.

使い方を体に覚えさせるため、積極的に使っていきたいと思います.

以下の記事を参考にしました.ありがとうございました.

具体例で学ぶ、Pythonのclassの使い方 | 株式会社キャパ CAPA,Inc.

【入門者向け】Pythonのクラスを習得する方法を詳しく解説! | CodeCampus

Pythonでclass(クラス)を使う方法【初心者向け】 | TechAcademyマガジン

【Python入門】クラスの使い方やオブジェクト指向の概念を理解しよう | 侍エンジニア塾ブログ | プログラミング入門者向け学習情報サイト

「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典:インスタンス