こんにちは。
この記事ではPythonにおけるオブジェクト指向の2部構成のうち、後半となります。
前回のPythonにおけるオブジェクト指向の前半部分については以下の記事を参照にしてください。
Pythonにおけるオブジェクト指向について解説【入門編その①】
続きを見る
今回は、オブジェクト指向の特徴である以下3つについて説明していきます。
本記事の学習目標
- 継承
- 多様性
- カプセル化
これらのコードの書き方に対する考え方は必須ですので、しっかり習得していきましょう。
オブジェクト指向の特徴 「継承」
まずは、継承についてです。
継承とは、あるオブジェクトが他のオブジェクトの特性を引き継ぐ事です。
» wikより引用
しかし、単に引き継ぐと言われてもわかりにくいと思います。
「継承」について具体的に考える
継承とは具体的に、前回の犬のオブジェクトから考えると、その上位概念になるものを考えます。
また、あるクラスの特徴を他のクラスが受け継いだ上で、さらに他の特徴も加わることを言います。
A
というクラスがあり、B
というクラスが A
を継承する場合、A
を親クラス(スーパークラス)、B
を子クラス(サブクラス) と呼んでいます。
継承を利用して、定義がほぼ同じのクラスは、その定義を親クラスに集約させ、子クラスで継承するという形でコードを記述すれば、同じようなことを何度も書く面倒さが無くなります。
上位概念としては、「動物」だと考え易いかと思います。
「動物」というオブジェクトには動物全てで考えられる属性と振る舞いを設定し、そして、それを継承します。(特性を引き継ぎます。)
「犬」「鳥」などにはそれぞれ固有の属性と振る舞いを設定します。
この「動物」「犬」「鳥」は以下のように呼ばれます。
「動物」を親クラス(スーパークラス、基底クラス)
「犬」「鳥」を子クラス(サブクラス、派生クラス)
具体的には下記図のような関係になります。
矢印は、親クラスを参照しますという意味になります。
Animal
としての属性である name
や、振る舞いの sleep
は子クラスの Dog
, Bird
でも使えます。
しかし、それぞれで特有な Dog
の breed
や Bird
の fly()
はそれぞれのクラスでしか使用ができません。
ですので、Dog
のオブジェクトで fly()
は使えずエラーになります。
次のコード例で動かして確認してみましょう。
Classを作成
まずは親クラスである、Animal
クラスを作りましょう。
前回の復習がてら、作ってみましょう。
コード例は下記ですが、参照しながら、実装してみてください。
In[]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | # 動物(親クラス) class Animal(): def __init__(self, name, weight): # 名前 self.name = name # 体重[kg] self.weight = weight # 寝る def sleep(self): # 寝る動作の処理コード print(self.name, '寝る') # 食べる def eat(self): # 食べる動作の処理コード print(self.name, '食べる') |
この Animal Class
は前回の Dog Class
同様普通に使うことができます。
使いかも復習がてら、実装してみましょう。
In[]
1 2 3 4 5 6 | # インスタンス animal = Animal('ペコ', '100') # プロパティを使う print(animal.name, animal.weight) # メソッドを使う animal.sleep() |
Out[]
1 2 | ペコ 100 ペコ 寝る |
どうでしょう?思い出してきたところで、新しい要素である。継承を使っていきます。
継承の使い方
親クラスの宣言は特に気にする必要はありません。
子クラスを宣言するときに、親クラスを継承しますよという意味で、子クラスのあとの括弧書きに親クラスを入れます。
コンストラクタ時の引数は必要に応じて数は変わりますが、子クラスでは親クラスの引数も取得して、super().__init__
で親クラスの引数を割り当てる必要があります。
super()
は親クラス(スーパークラス、基底クラス)のことになります。
In[]
1 2 3 | class 子クラス(親クラス): def __init__(self, 親_引数1, 親_引数2, 子_引数1): super().__init__(親_引数2, 親_引数2) |
Animal Classを継承してDog Classを作成
まずAnimal Classを継承して、Dog Classを作成します。
記述ルールに従って、以下の様に作成します。
- 子クラスの後のカッコ書きに親クラス名を追加 class Dog(Animal)
- コンストラクトのところにsuper().__init__(name, weight)と親クラスのコンストラクタを追加
実際のコードは下記になります。
In[]
1 2 3 4 5 6 7 8 9 10 11 12 13 | # 犬(Animalの子クラス) class Dog(Animal): def __init__(self, name, weight, breed): super().__init__(name, weight) # 犬種 ブルドック、ポメラニアンとか self.breed = breed # 走る def run(self): # 走る動作の処理コード print(self.name, '走る') |
早速使ってみましょう。
In[]
1 2 3 4 5 | # インスタンス dog2 = Dog(name='チョコ', breed='ビーグル', weight=10) # メソッド実行 dog2.sleep() # Animal Classで定義したメソッド dog2.run() # Dog Classで定義したメソッド |
Out[]
1 2 | チョコ 寝る チョコ 走る |
使い方は前回のDog Classと全く同じになりますが、今回はAnimal Classで定義したプロパティ、メソッドにもアクセスできることが分かりますね。
これが継承と呼ばれる仕組みです。
Animal Classを継承してBird Classを作成
同様にBird Classを実装します。Dog Classと同じように実装してみてください。
コード例、使い方は下記になりますので、わからなくなったら、参照してみてください。
In[]
1 2 3 4 5 6 7 8 9 10 11 12 13 | # 鳥(Animalの子クラス) class Bird(Animal): def __init__(self, name, weight, types): super().__init__(name, weight) # 鳥の種類 types = types # 飛ぶ def fly(self): # 飛ぶ動作の処理コード print(self.name, '飛ぶ') |
1 2 3 4 | # インスタンス bird1 = Bird(name='ピー', types='インコ', weight=1) bird1.sleep() # Animal Classのメソッド bird1.fly() |
Out[]
1 2 | ピー 寝る ピー 飛ぶ |
Dog Classのインスタンスでfly()を使ってみる
結果は予想通り、エラーになります。
In[]
1 | dog2.fly() |
Out[]
1 2 3 4 5 6 | --------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-13-dfbe86eda4be> in <module> ----> 1 dog2.fly() AttributeError: 'Dog' object has no attribute 'fly' |
Birdにあるflyメソッドは当然、Dogには実装していないので、そんな物はないと怒られるわけです。
オブジェクト指向の特徴 「多態性」
次に多態性について解説していきます。多様性とはポリモーフィズム(polymorphism)とも言います。
多様性を簡単に定義すると以下の様な意味合いになります。
オブジェクト指向の「多様性」の意味
- 同じメソッドを使い、オブジェクトによって異なる「振る舞い」をすること。
「多様性」を実現する方法に、次に説明するオーバーライドという方法あります。
オーバーライド
オーバーライドは子クラスで親クラスの宣言を上書きすることを言います。
言葉ではわかりにくいので、次で具体的に考えていきます。
オーバーライドの意味とは
多様性を実現する方法としてオーバーライドがあります。
オーバーライドは子クラスで親クラスの宣言を上書きすることです。
また、多様性とは同じメソッドを使用して、オブジェクトに異なる振る舞いをする事でした。
オブジェクトの「振る舞い」が異なるとは具体的にどういったことでしょうか。
例えば、動物は吠えたり鳴いたりします。
その振る舞いの事を make_sound() と表現ます。
鳥と犬では吠えたり、鳴いたりと違うので、make_sound() を実行しても異なる「振る舞い」の仕方になります。
鳥と犬との異なる振る舞いについて、以下の図を見て下さい。
以下がそのコードになります。
make_sound()
が追加してあり、他の箇所は省略していますが、今まで書いてあるコードが記載されているとしてください。
オーバーライドのコードの書き方
実際にオーバーライドのコードの書き方を記します。
make_sound()
が追加してあり、他の箇所は省略していますが、今まで書いてあるコードが記載されているとしてください。
具体的には、親クラスの Animal に make_sound()
メソッドを追加します。
親クラスのAnimalに子クラスでオーバーライドするための make_sound()
メソッドを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 | # 動物(親クラス) class Animal(): # --- 省略 --- # overridable # 吠える、鳴くなど def make_sound(self): # 子クラスでオーバーライドしなくても良い場合 # pass # 子クラスでオーバーライドして欲しい場合 raise NotImplementedError |
子クラスでのオーバーライドを必ずして欲しい場合とどちらでも良い場合があります。
その時は、passとraise NotImplementedErrorを使い分けてください。
- オーバーライドしてもしなくても良い場合:
pass
- オーバーライドを必ずして欲しい場合:
raise NotImplementedError
raise NotImplementedError
とすると、子クラスでオーバーライドしていないで使うとエラーが発生するようになります。
子クラスのDog Classにmake_sound()をオーバーライドする
先ほど作成した、Dog Classに make_sound()
をオーバーライドします。
オーバーライドは普通に関数を実装する方法と同じです。
In[]
1 2 3 4 5 6 7 8 9 | # 犬(Animalの子クラス) class Dog(Animal): # ---省略--- # 追加箇所 # 吠える def make_sound(self): print(self.name, 'ワン!ワン!') |
さて、使ってみましょう。Dogをインスタンスして、make_sound()
を使います。
In[]
1 2 | dog2 = Dog(name='チョコ', breed='ビーグル', weight=10) dog2.make_sound() # オーバーライドしたメソッド |
Out[]
1 | チョコ ワン!ワン! |
Dog Classでの make_sound()
ではワン!ワン!と吠える動作を実装できました。
次は、Bird Classにも実装します。
子クラスのBird Classにmake_sound()をオーバーライドする
同様に、Bird ClassにもDog Class同様に make_sound()
をオーバーライドして、使ってみてください。
下記が、コード例ですので、参照しながら実装してみてください。
In[]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | # 鳥(Animalの子クラス) class Bird(Animal): def __init__(self, name, weight, types): super().__init__(name, weight) # 鳥の種類 types = types # 飛ぶ def fly(self): # 飛ぶ動作の処理コード print(self.name, '飛ぶ') # 追加箇所 # 鳴く def make_sound(self): print(self.name, 'ピー!ピー!') |
1 2 | bird1 = Bird(name='ピー', types='インコ', weight=1) bird1.make_sound() |
Out[]
1 | ピー ピー!ピー! |
Bird Classではピー!ピー!と鳴く動作を実装できましたね。
print文のコメントが違うだけですが、このように、子クラスで異なる振る舞いをしたい場合にはオーバーライドを使って実装します。
オーバーロード
オーバーロードも多態性の一種だと言われていますが、最初に定義したものと少し意味合いが違ってきます。
オーバーロードは「引数、型などの違いで、同じメソッド名を複数つくること」を意味します。
例えば、下記のように func_sample
という同じメソッド名ですが、引数が1つのものと2つのものを作るといったことになります。
In[]
1 2 | func_sample(引数1) func_sample(引数1, 引数2) |
三角関数の tan()
を使用してオーバーロードを考えましょう。
- 引数1つの場合:
tan(x/y)
- 引数2つの場合:
tan(x, y)
オーバーロードを具体例で考える
犬のチョコちゃんに走ってもらうときに、「普通に走る」か「速く走ってもらうか」を指示するとします。
メソッドの run()
の引数でその速さを指定しますが、速く走ってもらう時は少ないので、引数がない場合は、普通に走ってもらうこととします。
オーバーロードの実装例
この実装方法は、通常の関数でも使う方法です。
引数=値をとして、引数に値を設定します。
In[]
1 2 | def 関数(引数=値): 処理コード |
では、実装例を見てみましょう。単純に、引数2つを足算する関数を用意しました。
1 2 3 | def func_samp(arg1, arg2=0): print(f'arg1:{arg1} arg2:{arg2}') return arg1+arg2 |
では3つのパターンに分けて使ってみます。
- 1つ目は
arg2
に値をいれていません。
この場合は、func_samp
で指定されたarg2=0
が使われます。 - 2つ目は
arg2
に0を入れます。なので1つ目と同じ結果になります。 - 3つ目は
arg2
に1を入れており、arg2
を1に書き換えて実行します。
In[]
1 2 3 | print(func_samp(1)) # arg2を指定せず、arg2はfunc_sampで指定された0が使われる print(func_samp(1, 0)) # arg2に0を指定、上と同じ結果 print(func_samp(1, 1)) # arg2に1を指定、arg2が1になって計算される |
Out[]
1 2 3 4 5 6 | arg1:1 arg2:0 1 arg1:1 arg2:0 1 arg1:1 arg2:1 2 |
Dog Classのrun()メソッドを変更
では、先ほどのDog Classの run()
メソッドに speed
引数を追加してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | # 犬(Animalの子クラス) class Dog(Animal): def __init__(self, name, weight, breed): super().__init__(name, weight) # 犬種 ブルドック、ポメラニアンとか breed = breed # 変更箇所 # 走る def run(self, speed = '普通'): # 走る動作の処理コード if speed == '普通': print(self.name, '普通に走る') elif speed == '速い': print(self.name, '速く走る') elif speed == '遅い': print(self.name, '遅く走る') # 吠える def make_sound(self): print(self.name, 'ワン!ワン!') |
では、実際に使ってみます。
- 1つ目は引数を指定しませんので、普通の速さで走ります。
- 2つ目は引数を指定して、早く走ってもらいます。
In[]
1 2 3 4 5 | dog2 = Dog(name='チョコ', breed='ビーグル', weight=10) # 引数指定なし dog2.run() # 引数に'速い'を指定 dog2.run(speed='速い') |
Out[]
1 2 | チョコ 普通に走る チョコ 速く走る |
これで、チョコちゃんに速く走ってもらったりと速さを指示できるようになりました。
補足
この関数に初期値を入れる方法は、オブジェクト指向とは関係なく、普通の関数を宣言するときもよく使われるものですので、しっかり習得しておく必要があります。
また、Javaとかの多言語でいうオーバーロードとは微妙に違うところもありますが、Python ではこの方法で実現するんだという認識で良いです。
オブジェクト指向の特徴 「カプセル化」
カプセル化はオブジェクト指向の考え方そのものです。
カプセル化 は、変数をオブジェクトの内部に隠すことを言います。
そうすることで、カプセル化された自分以外のオブジェクトがメンバ変数=プロパティを参照できない様にできます。
プログラムを一つのオブジェクトとして、使いやすいものにしようという考え方ですが、以下の図を参照にしてください。
カプセル化が使い易い理由としては、以下の理由があります。
- メソッドの機能などがわかりやすく、中身を知らなくても期待通りの処理が動く
- 使うのに必要なプロパティ、メソッドのみ使え、他は隠されている状態
2つ目は前回のオブジェクト指向の構成のプロパティ、メソッド、プライベート変数、メソッドがしっかり設定されていることを指します。
Pythonでは色々なライブラリをimportして使用しますが、これらのコードを見ることなく、メソッドを実行するだけ期待する処理が実行されます。
これは、きちんとカプセル化されている状態であるということです。
カプセル化は2つ目のことがよく言われます。前回のオブジェクト指向の構成のプロパティ、メソッド、プライベート変数、プライベートメソッドがしっかり作られてて、必要な機能が公開されていたり、隠されている状態です。
プロパティはよくゲッター、セッターというものでアクセス制限などを行うことがありますので、そこについて深掘りしていきます。
ゲッター、セッター
プロパティの役割としては、その属性の値を取得(ゲット)、設定(セット)することでその値を使用します。
前回、プロパティは変数で取り扱っていましたが、普通の変数と同じ扱いで、取得(ゲット)、設定(セット)が可能ですが、この取得と設定を関数で実現することがあります。
この値を取得(ゲット)する関数をゲッター、設定(セット)する関数をセッターと言います。
ゲッター、セッターを使う理由
意図せず値が変更されたりとバグの原因になることがあります。そのため、ゲッター、セッターを経由させることで、以下のことが行えます。
- 読み込みのみの制限
- 値の妥当性チェック
以上などを行う場合に使うことがあります。
ゲッター、セッターの作り方
ゲッター、セッターは以下のような記述ルールで作成します。
In[]
1 2 3 4 5 6 7 8 9 10 | # ゲッター @property def プロパティ名(self): 必要に応じて、値チェックなど処理記述 return 値 # セッター @プロパティ名.setter def プロパティ名(self, value): self.変数 = value |
ゲッターセッターには @property
や @プロパティ名.setter
がついていますが、そういったものだというくらいの認識で良いです。
ゲッターを使ってみる
プロパティをnameのみにして、ゲッターを追加します。下記になります。違うところは、下記2つです。
self._name
とプライベート変数で宣言@property
…のゲッター部分が追加
In[]
1 2 3 4 5 6 7 8 9 10 | class Dog(): def __init__(self, name): # 名前 ポチ、タマとか self._name = name # ゲッター @property def name(self): return self._name |
実際に使ってみましょう。使い方は通常のClassと同じです。
インスタンスして、プロパティの name
を表示してみましょう。
In[]
1 2 3 | # インスタンス dog1 = Dog(name='レオ') print(dog1.name) |
1 | レオ |
ここまでは、普通です。では、dog1.name
の値を変更してみましょう。
1 | dog1.name = "チョコ" |
エラーが出ます。これが、読み込みのみに制限されている状態です。
In[]
1 2 3 4 5 6 | --------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-27-92932c1f9d19> in <module> ----> 1 dog1.name = "チョコ" AttributeError: can't set attribute |
セッターを追加してみる
先ほどのセッターを追加して、書き込みもできるようにしましょう。
コードが以下になります。
In[]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class Dog(): def __init__(self, name): # 名前 ポチ、タマとか self._name = name # ゲッター @property def name(self): return self._name # セッター @name.setter def name(self, value): self._name = value |
これで、エラーが出ずに name
の値を変更できるようになります。
In[]
1 2 | dog1.name = "チョコ" print(dog1.name) |
Out[]
1 | チョコ |
補足
このnameというプロパティを記述するだけで、毎回記述することを考えると大変だと思います。
プロパティの取り扱いを厳格にしたい場合のみゲッターセッターを追加してあげるくらいで良いと思います。
Githubとかで見るコードでも、使っている人が少ないように感じますが、出会ったときにはこのことだと理解していただければと思います。
Pythonでは色々なライブラリをimportして使用しますが、これらのコードを見ることなく、メソッドを実行するだけ期待する処理が実行されると思います。
これは、きちんとカプセル化されている状態であるということです。
まとめ|Pythonにおけるオブジェクト指向について解説【入門編その②】
以上で2回に渡り、Pythonのオブジェクト指向について解説しました。
オブジェクト指向の総まとめの演習として、以下の課題がありますので、是非チャレンジしてください。
【Python】オブジェクト指向の練習問題【診療支援アプリ作成】
続きを見る
関連記事 機械学習は挫折しやすい【挫折が多い原因と挫折回避の方法を教えます】
関連記事 無料あり:機械学習エンジニアの僕がおすすめするAI(機械学習)特化型プログラミングスクール3社