Python

【Python】クラスの作り方とオブジェクト指向について学ぼう

クラスの作り方とオブジェクト指向について学ぼうPython

前回の記事でPythonにおける関数の作成方法とその呼び出し方について学びました。

今回はオブジェクト指向とPythonでのクラスの概念と定義方法と使い方について学んでいこうと思います。

「オブジェクト思考」って聞いたことあるよ、よくわからんけど

うん「オブジェクト思考」じゃなくて「オブジェクト指向」ね。
とりあえずこれから説明しますね。

この記事を読んで分かること
  • オブジェクト指向とクラスとは何かがわかる
  • Pythonでのクラスの定義方法や使い方がわかる
  • Pythonでのクラスの変数やメソッドの定義方法がわかる

スポンサーリンク

オブジェクト指向とクラスについて

前回の記事で関数を学びましたが、そもそも関数とは「いつでも呼び出せる特定の処理」です。

というのもプログラムとは上から順に1行1行処理をしていくのですが、関数によって処理順序に関係なくいつでも呼び出せるようになり、用途ごとにプログラムを切り離して組み立てることが出来るようになります。

こういったまとまった処理を関数化するというのは、処理の用途を明確化出来るとともにプログラムが処理ごとにまとまって分かりやすくなるという利点があります。

特にプログラムが大規模化・複雑化してくるとますます機能毎にまとまった処理や機能を持たせるという考え方が必要になってきます。

この関連する機能のまとまりをさらに大きくクラスとして定義し、そのクラスからオブジェクトを作りその組み合わせでプログラムを作ることでプログラム全体の構造をわかりやすくするといった考え方をプログラムでは「オブジェクト指向」と呼びます。

オブジェクト指向とは

オブジェクト指向

「ある役割を持ったモノ」ごとにクラス(プログラム全体の設計図)を分割し、モノとモノとの関係性を定義していくことでシステムを作り上げようとするシステム構成の考え方のこと。

オブジェクト指向プログラミングとは「オブジェクトを作ってそのオブジェクトを組み合わせてプログラムを作る」という考え方です。

例えば、シューティングゲームを作るとします。

シューティングゲームでは、味方の飛行機があって敵の飛行機がいくつも出てきます
それをビームを使って撃ち落とします敵は色々種類がいて敵毎に得点が違います
また、全ての敵を倒すと最後ボスキャラが出てきます、そのボスを倒すと1面ゲームクリア、その後2面3面へと続いていきます。
最後には大ボスが出てきて、それを倒すとエンディングが流れます。

この場合、どうやってプログラミングしますか。
1面から自分の飛行機と10秒後にこの敵を出して、当たったら得点計算して、当たらなかったら敵を移動させて。。。
みたいな処理をしていくととんでもなく処理が複雑になります。

ではなく、ある程度のまとまりでプログラムを構成します。
例えば

  • 味方の飛行機(十字キーで操作、ボタンを押すとビームが出る、敵のタマに当たると爆破する)
  • 敵の飛行機(種類がある、種類によってタマの打ち方や速さが違う、倒した時の得点も違う)
  • 画面のスクロール(敵が出るタイミング、スクロールの速さ)
  • 各面の構成(1面〜10面まである、各面によって難易度が異なる)
  • ボスキャラ

など、ある程度機能を整理してまとまり毎にオブジェクトを作って、そのオブジェクトを組み合わせてプログラムを作ります。

こうすることで、大きなプログラムでも機能毎に整理することができるため、プログラムがシンプルになります。

これが、オブジェクト指向の最大の利点であり、特徴でもあります。

クラスとインスタンス

ではオブジェクトとは具体的に何でしょうか。

オブジェクトにはクラスという基盤となるものがあり、そのクラスを元にオブジェクトを作成します。

このクラスから作成したオブジェクトをインスタンスと呼びます。

クラスには値を保持する変数と、処理を実施する関数のような(オブジェクト指向ではメソッドと呼びます)ものがあります。

実際の処理はクラスに保持されている変数やメソッドを使って処理しますが、具体的な処理はインスタンス毎に実施します。

先ほどのゲームで言うと、敵キャラというクラスがあり、飛行機にタマを打ってくる、撃ち落とされないように移動する、撃ち落としたら得点が入ると言うのは敵キャラ共通ですが、敵によって複数出てきたり、強い敵や弱い敵もいます、また撃ち落とされたら敵の数や強さによって得点も異なります。

そういった具体的に出てくる様々な敵キャラはインスタンスとして複数作成します。

ゲームで例えられてもよくわからないので、お料理で例えてもらえますか

料理ですね、うーん。

イメージとしては、たい焼きに似ているかと思います。

たい焼きって1個1個鯛の形を手作りして焼いてるわけじゃなくて、たい焼きの型で焼いてそこからたくさんたい焼きを作ってるじゃないですか。

型に生地を流してアンコとかクリームとか入れて作りますよね、たい焼きは1個1個違いますけど大元の型は同じですよね、こんな感じでたい焼きをたくさん作って組み合わせてプログラムを作ります。

なので以下のようなイメージを持ってもらえると良いかと。

  • たい焼きの型・・・クラス
  • 個々のたい焼き・・・インスタンス
クラスとインスタンスをたい焼きで例えた画面

こんなイメージですが伝わりましたでしょうか。

はい、たい焼きが食べたくなりました、今から買ってきますね

あーそっすか、そりゃよかった。

クラスのメンバ

クラス内には、クラスを構成するものが定義できまして、これらをクラスのメンバと呼びます。

クラスのメンバは以下の2つしかありません。

  • 変数:インスタンス毎に異なる値やインスタンス共通の値を格納出来る。
  • メソッド:クラスで用意される関数、インスタンス変数の値を利用出来る。

なので、クラスを作成する場合は、上記2つを作成することになります。

スポンサーリンク

クラスの作成

では具体的にクラスを作成して利用してみましょう。

クラスの定義

まずはクラスの定義からです。

class クラス名 :
    …クラスの内容…

クラスを定義する際は「class」の後に具体的なクラス名をつけて最後にコロン(:)を記述します。

それから次行よりインデントをつけてクラスの内容を記述します。

インスタンスの作成

では作成したクラスのインスタンスを作成する場合はどうでしょうか。

変数 = クラス名()

定義したクラス名に()をつけて呼び出します

これでインスタンスは作成されますので、そのまま変数に代入し、以後はこの変数を使ってインスタンスを操作します。

変数の定義

クラスのメンバーとして変数とメソッドが定義できます。

ここでは変数を定義してみましょう。

ただし、変数は2種類あり、どちらを使うかで定義方法が異なります。

  • クラス変数:クラスで保持している変数
  • インスタンス変数:インスタンスで保持している変数

どう違うんでしょうか

クラス変数

クラス変数はクラスで保持しているため、インスタンスに関わらずクラス内に1つだけの値となります。

class クラス名 :
    変数 = 値

    …クラスの内容…

クラス.変数
クラス.変数 = 値

クラス変数の定義は、クラス内で変数宣言をすれば作成されます。
値を取得する場合は「クラス名」にドットをつけて変数名を記述してアクセスします。

では、実際にクラスの定義とクラス変数を使用してみましょう。

class Memo:
    message = "OK"

print(Memo.message)
Memo.message = "Hello!"
print(Memo.message)

Memoクラスを作成し、クラス内にクラス変数を作成して変数を表示しています。
またクラス変数の値を更新して再度クラス変数の値を表示しています。

OK
Hello!

このようにインスタンスに関係なく、クラスで保持している変数を定義したい場合は上記のようは記述方法になります。

インスタンス変数

一方インスタンス変数はインスタンス単位で保持しているため、インスタンス数だけユニークに存在する変数で、あるインスタンス変数を更新しても別のインスタンスの変数は変更されません

def __init__(self, 値) :
    self.変数 = 値

インスタンス.変数
インスタンス.変数 = 値

インスタンス変数の定義は上記「__init__」メソッド内で変数を定義します。

この「__init__」メソッドはクラスのインスタンスが作成される際に自動的に呼び出されるメソッドでありこのメソッドを「コンストラクタ」と呼びます。

また引数に「self」という引数が設定されていますが、これは必須の引数でインスタンス化されたオブジェクト自身を指しています

上記の定義では、コンストラクタに渡された引数を使ってインスタンス変数を初期化していますが、引数ではなくデフォルト値を指定してももちろん構いません。

ではインスタンス変数を使った簡単な処理を見てみましょう。

class Memo:
    def __init__(self,str):
        self.message = str

memo1 = Memo("Hello!")
memo2 = Memo("Hi!")
print(memo1.message)
print(memo2.message)

memo1.message = "ByeBye!"
memo2.message = "See you!"
print(memo1.message)
print(memo2.message)

Memoクラスのインスタンスを2つ作成して、インスタンス変数を出力しています。
その後、各インスタンス変数を変更し再度表示しています。

Hello!
Hi!
ByeBye!
See you!

上記から、各インスタンス毎に変数を保持しており、インスタンス変数を変更しても他の変数に影響がないことが分かります。

クラス変数時の注意

クラス変数を定義してアクセスする際に、以下のようにインスタンスからクラス変数を参照することも可能です。

class Memo:
    message = "OK"

memo1 = Memo()
print(memo1.message)
memo2 = Memo()
print(memo2.message)

ですが、これはっきり言っておすすめしません。

なぜかというと、これはPythonにてインスタンス変数「message」がない→クラス変数「message」を自動で探して表示しています

例えば、次のように9行目でインスタンスの「message」に値を代入すると、一見クラス変数の値を代入しているように見えますが、実はインスタンス変数「message」を新たに変数定義しています。
(要するにクラス変数とインスタンス変数が同一名の変数を保持することになります)

class Memo:
    message = "OK"

memo1 = Memo()
print(memo1.message)
memo2 = Memo()
print(memo2.message)

memo1.message = "Hello!"
print(memo1.message)
print(memo2.message)

Memo.message = "Good Night!"
print(memo1.message)
print(memo2.message)

上記を実行すると以下となります。

OK
OK
Hello!
OK
Hello!
Good Night!

つまり、memo1インスタンス側では途中からインスタンス変数の「message」が追加されており、インスタンス変数側を参照しています。一方memo2はインスタンス変数「message」が定義されていないので、ずっとクラス変数「message」を参照し続けています

。。。よく分かりませんね

はいその通りで、処理が分かりにくいしこういう処理はバグの温床となりますので、クラス変数は絶対にインスタンス変数からアクセスしないでください。

なので、クラスで変数を使う場合は、上記のクラス変数とインスタンス変数の定義方法以外は使わないようにしましょう

筆者も最初クラスの変数定義に関しては相当混乱しました、特にJava経験者にはちょっと意味がわからない振る舞いになりますので、上記でクラス変数、インスタンス変数で定義した方法を使いましょう。

プライベート変数

最後はインスタンス変数を定義する場合に、変数を外部から変更したくない場合に変数をカプセル化することが出来ます。これをプライベート変数と言います。

プライベート変数は変数名の前にアンダースコアを2つ付けて定義します。

def __init__(self, 値) :
    self.__変数 = 値

こうすることでプライベート変数はインスタンスの外から変更出来なくなり、クラスの中のメソッドからしか変更出来なくなるため、より安全にプログラムの作成が可能となります。

メソッドの定義

クラスのメンバとして変数同様にメソッドも定義できます。

メソッドも変数同様2種類あり、どちらを使うかで定義方法が異なります。
ただし、一般的にメソッドと言うと下記インスタンスメソッドを指します。

  • クラスメソッド:クラスで保持している関数(メソッド)
  • (インスタンス)メソッド:インスタンスで保持している関数(メソッド)

(インスタンス)メソッド

メソッドは関数をクラスのメンバとして用意したもので、基本的な書き方は関数と同じです。

class クラス名 :
    def メソッド名(self, 引数):

        …メソッドの実行文…

こちらも先程のコンストラクタ同様に引数に「self」がありまして、こちらは必須の引数となります。

selfとは「このインスタンス自身」を指していて、このselfの後にドットをつけてメソッド名を記述してアクセスします。

ではメソッドを定義してみましょう。

今回のサンプルプログラムは、先程説明したプライベート変数を使ってメソッド経由で変数にアクセスしてみましょう。

class Memo:
    def __init__(self):
        self.__num = 123
        self.message = "Hello!"
    
    def print(self):
        print(str(self.__num) + ":" + self.message)

memo1 = Memo()
memo2 = Memo()

memo1.__num = 100000
memo1.message = "Bye!"

memo1.print()
memo2.print()

6行目でクラス内の変数を表示するprintメソッドを定義しています。

ただし、変数「__num」はプライベート変数のため12行目で変数に値を入力していますが、実際に出力される値は変更されていない点に注目して下さい。

123:Bye!
123:Hello!

クラスメソッド

クラスメソッドはクラス変数同様クラスで保持しているため、インスタンスに関わらずクラス内に1つだけのメソッドとなります。

また、クラスメソッドはクラス変数しかアクセス出来ません。インスタンス変数にはアクセス出来ないので注意が必要です。

class クラス名 :
    @classmethod
    def メソッド名(cls, 引数):

        …メソッドの実行文…

クラスメソッドの定義はメソッド定義の前に「@classmethod」という一文を追記します。

こういった「@」で始まるプログラム文をアノテーションと言います。

クラスメソッドの定義は引数のselfの代わりに「cls」という引数が用意されます。

では、実際にクラスメソッドを使ってみましょう。

class Memo:
    message = "OK"

    @classmethod
    def print(cls):
        print(cls.message)

Memo.print()
Memo.message = "Good Night!"
Memo.print()

上記を実行すると以下となります。

OK
Good Night!

プロパティ

クラスを作成する理由の一つとして変数をカプセル化して他からブラックボックス化できるのですが、その変数の値をセットしたり取得する際にいちいちメソッドを定義しなければならないというめんどくささがあります。

そこで、値をセットするメソッドや値をゲットするメソッドを用意せずに変数に直接アクセスするような感覚でセット、ゲットメソッドを裏で実行するような仕組みがPythonには用意されていて、それがプロパティと呼ばれるものです。

カプセル化する際にはプロパティを使うことをお勧めしますので、一応ここで説明しておきます。

ちょっと難しいなと思う方はここは無理して覚えなくて良いですよ、何となくこんなのあるんだくらいで大丈夫です。

プロパティは一言で言うと「メソッドでアクセスできるインスタンス変数」です。クラス側はメソッドで定義しますが、利用する際はインスタンス変数を直接アクセス出来るような動作をします。

    @property
    def プロパティ名(self):

        return 値

    @プロパティ名.setter
    def プロパティ名(self, 値):

        …値を設定する処理…

プロパティはアノテーションを使って定義します。

では実際にプログラムを書いてみましょう。

class Memo:
    def __init__(self, num):
        self.__num = num

    def __str__(self):
        return "<Memo: num=" + str(self.num) + ">"

    @property
    def num(self):
        return self.__num
    
    @num.setter
    def num(self, value):
        if isinstance(value,int):
            self.__num = value

memo = Memo(123)
print(memo.num)
memo.num = "aaa"
print(memo)
memo.num = 345
print(memo)

Memoクラスにプライベート変数「__num」を定義して、その変数のアクセスにプロパティを使用しています。

「__num」は数字ですのでint型のみ設定できるように、14行目のセッターメソッド内でint型か判定してintであれば値を設定しています。

19行目で文字列「aaa」を設定しますが、int型では無いためセッターメソッド内で変数に設定されないためコンストラクタで設定した数値が表示されています。

上記を実行すると以下となります。

123
<Memo: num=123>
<Memo: num=345>

上記のようにカプセル化してセッター/ゲッターメソッドを設定する際はプロパティを使えばスマートに書くことが出来ますし、クラスを使う側もシンプルに使うことが出来ます

スポンサーリンク

継承

これまでは、クラスを作成する方法について記載しましたが、新しいクラスを作成する際にあるクラスの機能の一部を変更したクラスを作りたい場合はどうでしょうか。

その際に、元のクラスの変数やメソッドを全てコピー&ペーストして変更部分だけ修正してクラスを作ったとします。

そうすると似たような処理のクラスが複数できてしまい、例えば同じ箇所にバグがあった場合にその両方を修正する必要が出てきます。
また既存の箇所を修正する場合も同様で、複数の同じ箇所を同様に修正する必要があります。

これがコピーするクラスが10個、100個であればとても管理出来ません。

そういった場合を考慮して、オブジェクト指向では継承というものがあります。

Pythonでもクラス定義の際にはこの継承が使えますので、ここではクラスの継承の仕方を説明します。

継承の定義方法

継承とは、既にあるクラスの全ての機能を受け継いで新しいクラスを定義することです。

継承は以下のようにクラスを定義します。

class クラス名(継承するクラス名):
    …クラスの実行文…

クラス名の後に()をつけてそこに継承したいクラス名を指定します。
これでクラスの機能を継承した新しいクラスが定義できます。

継承を利用するときに継承する元となったクラスのことを「基底クラス」と呼びます。
また基底クラスを継承して新たに作成されたクラスを「派生クラス」と呼びます。

では、実際に継承を利用してみましょう。

class Person:
    name = "name"

class Child(Person):
    age = 12

print(Person.name)
print(Child.name+":"+str(Child.age))

実行結果は以下です。

name
name:12

基底クラスのPersonクラスと派生クラスのChildクラスを作成し、各クラスにクラス変数を定義します。

そして、派生クラスのChildクラスからPersonクラスの「name」変数にアクセスし表示しています。

これは継承により派生クラスは、派生クラスの変数と基底クラスの変数の両方が使えるためです。

オーバライド

ではもう少し継承の機能を見ていきましょう。

継承すると派生クラスに基底クラスと同じ名前のメソッドがあると、派生クラスのメソッドで上書きされます

これをオーバライドと言います。

オーバライドを利用することで、派生クラスでは基底クラスにある機能を利用しつつ一部の機能を変更することができます。

class Person:
    def __init__(self,name):
        self.name = name

    def __str__(self):
        return "[Person]My name is "+self.name+"."

class Child(Person):
    def __str__(self):
        return "[Child]My name is "+self.name+". "+"I am 12 years old."

ps1 = Person("Taro")
print(ps1)
ps2 = Child("Sakura")
print(ps2)

実行結果は以下となります。

[Person]My name is Taro.
[Child]My name is Sakura. I am 12 years old.

基底クラスのPersonクラスと派生クラスのChildクラスを作成します。

そして派生クラスに基底クラスと同じ「__str__」メソッドを作成すると、コンストラクタはChildクラスのメソッドを使いますが「__str__」メソッドの呼び出しではPersonクラスのメソッドが呼ばれないで、派生クラスのメソッドが呼ばれておりオーバライドされていることが分かります。

スポンサーリンク

まとめ

今回は、Pythonにおけるクラスの定義方法や実際の使い方などを説明しました。

コメント

タイトルとURLをコピーしました