オブジェクト指向のプログラミングを覚えることで大規模なコードを効率よく記述することが可能です。ここではPython初心者である筆者がPythonのクラス(class)の使い方とクラスを使うメリットを学習した結果をまとめます。
こんにちは。wat(@watlablog)です。Python関連のブログを開設して約1年が経ちますが、ここではいよいよPythonクラス(class)を学んでみます!
クラス(class)とは?
クラスの概念
プログラミングにおけるクラス(class)とは、オブジェクトを生成するために必要な設計図に相当するものです。
クラスは設計図であるため、そのオブジェクトがどういったデータを必要とし、どのような振る舞いをするかといったことを記述する必要があります。
また設計図は所詮方法を述べたものにしか過ぎないため、実際に使うためには実体を作ってから動作させる必要があります。
設計図を使って実体を作る様から、しばしばクラスはたい焼きを作るような「金型」、実体のことを実際に材料を流し込んで作った「商品」と表現されることがあります。
クラスはデータや関数をまとめることが出来るため、いたずらに変数を増やしたり、同じコードをあちこちに書いたりしなくても良くなるメリットがあります。
プログラミングに限った話ではありませんが、コードはKISSの原則(Keep It Short and Simple:短く単純にしておくように)に則ることが正とされています。
クラスを使いこなすことでKISSの原則に従うプログラミングが可能になるはずです。
クラスと関数の違い
これまで当WATLABブログではPythonプログラミング関連で150記事以上紹介してきましたが、コードの効率化という面ではdef関数しか使っていません。
※def関数については「Pythonの関数 def文の使い方!引数や別ファイル式も解説」を参照下さい。
関数は計算を1つにまとめることが出来ますが、クラスは複数の関数もまとめることが可能であり、関数の上位の存在となります。
クラスの中で使われる関数のことをメソッドと呼びます。メソッドについては後程説明します。
Pythonにおけるクラスの作り方(コードで解説)
それでは早速Pythonプログラムでクラスを定義しながら、クラスがどんなものなのかを見ていきたいと思います。
僕自身プログラミングは初心者(専門的に教育を受けたことは無いし、仕事で使ったとしても我流レベル)なので、今回は色々なWebページや書籍を参照しました。
しかし、「Pythonのクラスはこうやって定義します」「クラスは〇〇を定義して…」といった手段ベースの説明が多く、クラスを使うと一体どんなメリットがあるのか、サンプル以外のクラスはどうやって設計するのかといった内容が一発で理解できるような説明は中々見当たりませんでした(探し方が悪いとか、頭が固いとかもあるかもですが…)。
ここでは最終的に僕が理解した内容を、実際にクラスを定義する流れに沿って説明していこうと思います。そのため、中には間違った表現を使っている所があるかも知れません。もしお気付きの点がございましたらコメントを頂けたら助かります!
はじめにクラスの設計が必要
早速と言いましたが、Pythonクラスの記述方法を語る前に、まずはクラスを設計する作業が必要です。
多分ここが一番重要なポイントだと思います。適切に設計しないと、せっかくコードを効率的に書こうと思っても他人が理解し難かったり、後で仕様追加がし難いクラスが出来上がってしまうかも知れません。
仕様決め
…とは言えここでは簡単に、「三角形の計算をまとめたクラス」で説明をしていきます。下図に今回作るクラスの概要を示します。
ここではクラスに
・line1
・line2
という2つのデータを渡して、
・三角形の斜辺の長さlen
・三角形の面積area
という2つの計算値を求める関数をまとめます。
内容自体はdef関数でも簡単に書くことが出来ますが、クラスで作っておけば後で別の計算を追加したい時に、クラス内の関数を追加することで共通の変数を使うことが可能になります。
def関数の場合は追加した関数に計算をさせる時に、再度変数を渡さないとなりません。クラスで作っておけば変数を共有してどんどん追加修正が出来るというメリットがあるみたいですね。
今回は上記クラスを実際にPythonで作ってみます。
クラスの命名規則
始めに、クラスの名前を決めます。
クラスは、それがクラス名であることがわかるように一般的に暗黙の了解となっている命名規則があるそうです。
以下の図にPythonのクラスで推奨されているクラス名の付け方を示します。
def関数の場合はfunction_nameのように、小文字の英単語とアンダーバーを組み合わせて名前を付けるのが良いらしいですが(知らなかった…)、クラスの場合は英単語の先頭を大文字にすることで単語の区切りを明確にし、def関数と区別をするそうです。
それでは命名規則に従って、三角形の計算をするクラスを作ってみます。
今回は以下のコードのように「TriangleCalc」と命名してみました。
1 |
class TriangleCalc: |
コンストラクタとプロパティの記述をする
クラスに命名をしたので、次は中身を作っていきます。
次に作るのはコンストラクタ(Constructor)です。
コンストラクタとは、設計図としてのクラスを使って実体(インスタンス)を生成する際に最初に実行される関数の事です。主に初期化に使われることから初期化メソッドとも呼ばれます。
今回作成するクラスのコンストラクタの宣言部分を以下のコードに示します。defの前にインデントが必要なことに注意して下さい。
1 2 |
class TriangleCalc: def __init__(self, line1, line2): # コンストラクタ(初期化メソッド) |
Pythonのコンストラクタはdefを使って書きます。initをアンダーバー2個ずつで挟み、def関数で書くコンストラクタの名称は「__init__」という名称を使うことが決まりです。
クラスは後で実体(インスタンス)を作って使う時に、それぞれ固有の名称を付けることが出来ます。どんな名称が設定されても自分自身のデータを参照することが出来るよう、このdef関数(メソッド)の第1引数には「self」を指定するのが決まりです。
その他の引数には、それぞれ上で設計したline1, line2を設定します。
もしクラスで扱うメソッドを追加し、新たな引数が必要になった場合はこの括弧内に追加していきます。
続いて、コンストラクタに初期化の内容を記載します。
初期化が必要なのは変数です。ここで設定した変数群はクラスのプロパティ(Property)と呼ばれます。
以下のコード例のように、引数として参照している値を変数に格納したり、これからメソッドで計算するアウトプットを入れる変数の初期値を決めたりといった作業を行います。
1 2 3 4 5 6 |
class TriangleCalc: def __init__(self, line1, line2): # コンストラクタ(初期化メソッド) self.line1 = line1 # 引数line1を設定 self.line2 = line2 # 引数line2を設定 self.len = 0 # lenを初期化 self.area = 0 # areaを初期化 |
ここで、先ほどと同様にクラスは生成されたインスタンス毎にこの変数を使い分けたいので、変数名には「self.」を付けて定義する決まりがあります。
そういえば、外部ライブラリを使う時もよくこのドット「.」を使っていましたね。クラスの中身を理解することで、プログラム内で今どんな作業をしているかがクリアになってきそうです。
メソッドを追加する
コンストラクタを書いた後は、メソッド(クラス内で使用する関数)を追加していきます。ここはdef関数の書き方と同じなので、もしdef関数の書き方に不安がある方は「Pythonの関数 def文の使い方!引数や別ファイル式も解説」をご覧下さい。
但し、コンストラクタ部の変数を使う時に、「self.」を頭に付ける所が注意点です(メソッドで新たに引数を設定する場合はselfは要りません)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import numpy as np class TriangleCalc: def __init__(self, line1, line2): # コンストラクタ(初期化メソッド) self.line1 = line1 # 引数line1を設定 self.line2 = line2 # 引数line2を設定 self.len = 0 # lenを初期化 self.area = 0 # areaを初期化 # 斜辺の長さを求めるメソッド def calc_len(self): self.len = np.sqrt(self.line1 ** 2 + self.line2 ** 2) return self.len # 面積を求めるメソッド def calc_area(self): self.area = (self.line1 + self.line2) / 2 return self.area |
以上で基本的なクラスが出来上がりました。
以下より作ったクラスを実際に使ってみましょう。
Pythonにおけるクラスの使い方(コードで解説)
インスタンスを生成する
作ったクラスを実際に使うためにインスタンスを生成します。
インスタンスの生成は、「インスタンス名=クラス名(引数)」で行います。
以下のコードに例を示します。今回は大きさの違う三角形を2つインスタンス化してみました。
1 2 3 |
# インスタンス化 Triangle1 = TriangleCalc(line1=1, line2=1) Triangle2 = TriangleCalc(line1=10, line2=10) |
メソッドを実行する
最後にメソッドを実行して結果の値を取り出してみます。
メソッドの実行は、先ほど生成したインスタンス名とメソッド名をドットで繋ぎ「インスタンス名.メソッド名(引数)」と書きます。
今回はコンストラクタ部で引数を渡していますが、追加で引数が必要である場合はこの括弧内に配置させます。
1 2 3 4 5 6 7 8 9 10 11 |
# メソッドを実行 Triangle1.calc_len() Triangle1.calc_area() Triangle2.calc_len() Triangle2.calc_area() # クラスから変数を取り出して表示 print(Triangle1.len) print(Triangle1.area) print(Triangle2.len) print(Triangle2.area) |
計算された変数を取り出すためには、「インスタンス名.変数名」と書きます。
サンプルクラス構築と結果確認までの全コード
ここまでのクラス構築から実行までの全コードを繋げたもの(コピペ用)を以下に示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import numpy as np class TriangleCalc: def __init__(self, line1, line2): # コンストラクタ(初期化メソッド) self.line1 = line1 # 引数line1を設定 self.line2 = line2 # 引数line2を設定 self.len = 0 # lenを初期化 self.area = 0 # areaを初期化 # 斜辺の長さを求めるメソッド def calc_len(self): self.len = np.sqrt(self.line1 ** 2 + self.line2 ** 2) return self.len # 面積を求めるメソッド def calc_area(self): self.area = (self.line1 + self.line2) / 2 return self.area # インスタンス化 Triangle1 = TriangleCalc(line1=1, line2=1) Triangle2 = TriangleCalc(line1=10, line2=10) # メソッドを実行 Triangle1.calc_len() Triangle1.calc_area() Triangle2.calc_len() Triangle2.calc_area() # クラスから変数を取り出して表示 print(Triangle1.len) print(Triangle1.area) print(Triangle2.len) print(Triangle2.area) |
このコードを実行すると以下の結果を得ます。
同一のクラス、メソッドを使い、同一の変数名で値を取り出しているにも関わらず、インスタンス固有の結果を得るということが出来ました。
1 2 3 4 |
1.4142135623730951 # Triangle1の斜辺の長さ 1.0 # Triangle1の面積 14.142135623730951 # Triangle2の斜辺の長さ 10.0 # Triangle2の面積 |
このように、同じような振る舞いをするもの(オブジェクト)を一つ一つ作らないで共通のメソッドや変数をまとめ、インスタンス毎に使いまわしが出来るようプログラミングすることをオブジェクト指向プログラミングと呼ぶそうです(色々説明の仕方があるため曖昧な表現ですが)。
オブジェクト指向の考え方はよくRPGのキャラクターや車に例えられています。
車をプログラム上で表現しようとした時、1台1台毎に振る舞いを書いていたら、もし修正が必要な時に同じ修正を全ての台数でしなければいけません。
オブジェクト指向プログラミングで書いておけば、機能追加で修正する個所はクラスの内部だけで済みます。
大規模プログラムになればなるほどオブジェクト指向プログラミングの考え方を使わないと大変なことになりそう!是非マスターしたい所ですね!
Pythonクラスのさらに便利な機能紹介(コードで解説)
上記まででPythonのクラス構築と使用の基本的な説明は終了です。使うだけであれば上記内容で十分ですが、クラス・オブジェクト指向の考え方にはまだまだ便利な機能があります。ここではいくつかの便利機能をPythonコードを書きながら紹介していきます。
デストラクタで安全なリソース管理をする
デストラクタ(Destructor)とは、コンストラクタの対義語であり、具体的には生成したインスタンスを削除するメソッドです。
デストラクタを使えば、プログラムが実行されている間にインスタンスを削除することが出来ます。
インスタンスを生成するということは、メモリを割り当てるアロケーション(Allocation)がされます。大規模プログラムであれば大量のインスタンスを生成することもあるでしょう。
生成したインスタンスが使用されなくなったら、適宜デストラクタで明示的に削除した方がリソースの有効な使い方が可能になります。
Pythonにおけるデストラクタは「__del__」で定義します。
1 2 3 4 5 6 7 8 9 |
class TriangleCalc: def __init__(self, line1, line2): # コンストラクタ(初期化メソッド) self.line1 = line1 # 引数line1を設定 self.line2 = line2 # 引数line2を設定 self.len = 0 # lenを初期化 self.area = 0 # areaを初期化 def __del__(self): # デストラクタ print('Deleted.') |
デストラクタの実行は「del インスタンス名」と書きます。
1 |
del Triangle1 |
但し、Pythonはガベージコレクション(Garbage Collection)という不必要なメモリを自動で解放してくれる機能を持っています。
オブジェクトの種類(Python以外の言語を使用したライブラリ起因のオブジェクト等)によっては解放してくれないものもあるそうなので、デストラクタの使用はメモリに関する一定の知識が必要であると考えられます。
継承で既に作ったクラスを再利用する
クラスを使う上でおそらく最も効率向上に有効な概念に「継承(Inheritance)」があります。
継承とは、既に作成したクラスを受け継いで新しいクラスを作ることです。
継承することで親クラスが持つメソッドや変数を子クラスから利用することが可能です。
以下に継承を含んだサンプルコードを示します。
ここでは先ほどのTriangleCalcクラスを継承したTriPrismクラスを追加しています。継承は「class 子クラス名(親クラス名):」で行います。
TriPrismクラスには高さheightを新たな引数として設定し、親クラスで計算した面積areaを使って体積volumeを計算するメソッドを記述しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
import numpy as np class TriangleCalc: def __init__(self, line1, line2): # コンストラクタ(初期化メソッド) self.line1 = line1 # 引数line1を設定 self.line2 = line2 # 引数line2を設定 self.len = 0 # lenを初期化 self.area = 0 # areaを初期化 # 斜辺の長さを求めるメソッド def calc_len(self): self.len = np.sqrt(self.line1 ** 2 + self.line2 ** 2) return self.len # 面積を求めるメソッド def calc_area(self): self.area = (self.line1 + self.line2) / 2 return self.area class TriPrism(TriangleCalc): # 体積を求めるメソッド def calc_volume(self, height): self.volume = self.area * height return self.volume # インスタンス化 prism = TriPrism(line1=1, line2=1) # メソッドを実行 prism.calc_len() prism.calc_area() prism.calc_volume(height=10) # クラスから変数を取り出して表示 print(prism.len) print(prism.area) print(prism.volume) |
以下が実行結果です。
このように継承を行うことで、子クラスで親クラスのメソッドを使えるようになります。
1 2 3 |
1.4142135623730951 # 親クラスのメソッドで計算された斜辺の長さ 1.0 # 親クラスのメソッドで計算された面積 10.0 # 子クラスで新たに定義したメソッドで計算された体積 |
この程度であれば「親クラスを編集してvolumeを計算させるメソッドを追加すれば良いじゃないか」と思われますが、大規模なプログラムになるとクラスは様々な場所で使われます。子クラスを作っておけば親クラスを編集しないので、他のアルゴリズムに対するバグの元を作ることにならないメリットがあります。
super()を使った継承の例
Pythonで継承を行う時は、super()を使うことが推奨されています。以下のコードは上記継承コードと全く同じ動作をしますが、子クラスTriPrismでsuper()を使っている所が異なります。
子クラスのコンストラクタ部で「super().__init__(継承したい変数)」と定義しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
import numpy as np class TriangleCalc: def __init__(self, line1, line2): # コンストラクタ(初期化メソッド) self.line1 = line1 # 引数line1を設定 self.line2 = line2 # 引数line2を設定 self.len = 0 # lenを初期化 self.area = 0 # areaを初期化 # 斜辺の長さを求めるメソッド def calc_len(self): self.len = np.sqrt(self.line1 ** 2 + self.line2 ** 2) return self.len # 面積を求めるメソッド def calc_area(self): self.area = (self.line1 + self.line2) / 2 return self.area class TriPrism(TriangleCalc): def __init__(self, line1, line2, height): super().__init__(line1, line2) self.height = height self.volume = 0 # 体積を求めるメソッド def calc_volume(self): self.volume = self.area * self.height return self.volume # インスタンス化 prism = TriPrism(line1=1, line2=1, height=10) # メソッドを実行 prism.calc_len() prism.calc_area() prism.calc_volume() # クラスから変数を取り出して表示 print(prism.len) print(prism.area) print(prism.volume) |
実行結果は先ほどと同じになるので割愛しますが、super()は多重継承という複数のクラスを継承する時に特に推奨されているそうです。この内容は結構上級レベルになりそうなので、別記事でそのうちまとめようと思います。
オーバーライドで内容を書き換えて継承する
コーディング効率を向上させるクラスの機能の1つにオーバーライド(Override)があります。
オーバーライドとは、親クラスを継承はするけど、メソッドやプロパティの一部を変更するといういわゆる上書き機能のことです。
以下のコードはこれまで紹介してきたコードの子クラス部分のみを紹介しています。親クラスであるTriangleeCalcを継承していますが、斜辺を求めるcalc_lenをオーバーライドしています。
Pythonのオーバーライドの方法は「メソッドやプロパティに同じ名前を使う」ことで実現されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class TriPrism(TriangleCalc): def __init__(self, line1, line2, height): super().__init__(line1, line2) self.len = 1 self.height = height self.volume = 0 self.dimless_len = 0 # 体積を求めるメソッド def calc_volume(self): self.volume = self.area * self.height return self.volume # 斜辺の無次元長さを求めるメソッド(オーバーライド) def calc_len(self): self.len = np.sqrt(self.line1 ** 2 + self.line2 ** 2) self.dimless_len = self.len / self.line1 return self.len, self.dimless_len |
オーバーライドをすることで親クラスからの小変更をすることが出来るので、「かゆいとこに手が届く」ようにコードを書いていくことが可能になりますね。
まとめ
本ページではPythonのクラスの使い方を説明して来ました。
基本的なクラスは、
①クラスでまとめる変数と関数の仕様を設計する
②コンストラクタでプロパティの初期化を書く
③メソッドを書く
で構築することができます。
そしてクラスを使うためには、
①インスタンスを生成する
②メソッドを実行する
という手順が必要ということがわかりました。
上記基本的な使い方の他にも、
・デストラクタでリソースの節約が出来る
・クラスの継承で効率の良いコード再利用が出来る
・オーバーライドで親クラスの一部を変更した子クラスが作れる
といった様々なテクニックがあることもわかりました。
ここで紹介した使い方はクラスのほんの一部の内容にしか過ぎません。
クラスの恩恵を得るためには継承やオーバーライド、複数クラスの連携を自由自在に使いこなす必要があります。
今回僕はPythonにおけるクラスの基本を調べましたが、ようやく使い方がわかったレベルです。
そろそろ書籍やWebでも他の人の書いたコード(ライブラリの中身を見るのも良いかも)が理解できるようになってきたので、今後は上手い人のコードを真似るということにも挑戦してみようと思います。
オブジェクト指向は本当に奥が深いです!まだまだ練習あるのみ!
X(Twitter)でも関連情報をつぶやいているので、wat(@watlablog)のフォローお待ちしています!
コメント