見出し画像

【Python】クラスの継承・オーバーライドをしっかりと理解する

クラスの継承は

・親と子は似ている
・師範から生徒への技の伝承

と似ているように感じます。(えっ!?ヒロアカ思い出したのは僕だけ?)

クラスの継承ができれば、

同じようなクラスを書く必要

がなくなります。

それではまいりましょう。

基本編

クラスの継承は技の伝承?のごとく関数や変数を引き継げるのか?

クラスの継承、親から子へ
クラスを継承すると、OFAの継承者でいう技を引き継げるように、親クラスの関数や変数を使うことができます!!(クラスの継承をイラストで表すとこんな感じになりました。)

あるクラスをもとにして新たにクラスを作ることを

「継承」

といいます。継承ができると、

わざわざ同じような変数やメソッドを書く

必要がなくなります。
さきほど、序章のぶんにて

親と子はお互いに似ている

と書いたのは、 クラスの継承をわかりやすくたとえるためです。
そういえば、ヒロアカのOFAの継承者も技をひきつげられ、どんどん強力に
なっていくのを思い出しました🤔なので、ヒロアカ好きの方は、

「クラスの継承」=「OFAの継承」

ととらえるとこれから読まれる際に助けとなるかもしれません🤔💦

おいっ!?Python🐍のだろ!?

と思われた方、おまたせしました。Pythonの話題からそれてしまいましたが、書き方を見てみましょう。

親クラスをParent
子クラスをChild

としますと、

class Child(Parent): 
  <処理>

と書きます。
いったん、クラスの継承を行うと、

「親」→「子」

へとつないでいくので、

①親クラスのメソッドと変数は子クラスでもそのまま使える
②メソッドが同じ名前でもOK

のように2点のメリットが得られるという点です。
ただし、②のように

メソッドがかぶると、少しふるまいが異なります。

クラスのオーバーライドについて

親と子のメソッドの名前がかぶることを

オーバーライド

といいます。
メソッドがかぶると、

子クラスのメソッドが優先

されます。

したがって、

子クラスのメソッド

しか呼び出されません。
親クラスのメソッドも使いたい場合は、

super().[メソッド名]

を使います。(ヒロアカでは、OFAで自分流にアレンジにできる技があったけ!?とふと思ってしまいました。ヒロアカ詳しい人はぜひ教えてください。)

まず、コンストラクタ__init__()のオーバーライドを見ていきましょう。

class Teacher:
 def __init__(self):
   print('Teacher')

class Student(Teacher):
 def __init__(self):
   super().__init__()#←※
   print('Student')
s=Student()

<実行結果>

Teacher
Student

のようにきちんと

親から子へ処理されていること

がわかります。

ですが、コンストラクタの親メソッドを呼び出すには

super().__init__()

と書かないと、

親クラスのメソッドを呼んでくれません。

また、オーバーライドは

__init__()のようなコンストラクタ以外

にも使うことができます。
次のコードを見てみましょう。

class Teacher:
 def __init__(self):
   print('Teacher')
 def inherit(self):#←注目
   print('teacher inherit')

class Student(Teacher):
 def __init__(self):
   super().__init__()
   print('Student')
 def inherit(self):#←注目
   super().inherit()
   print('student inherit')

s=Student()
s.inherit()

実行結果

Teacher
Student
teacher inherit
student inherit

このようにコンストラクタだけではなく、

メソッドでもオーバーライドが使える

ということがわかります。親クラスのメソッドも使いたいときは、さきほどの__init__()、すなわちコンストラクタの考え方と一緒で

super().[メソッド名]()

でOKです。

実践編

結局のところ、クラスを継承するメリットってあるん?

では継承すると、どういったメリットがあるのでしょうか?

と趣味程度のちいさな開発しかしない私もふと長い間疑問に思っていました。ですが、

ストップウォッチとタイマー

を作ったときに、

「クラス」

の必要性をしっかりと認識するようになりました☺️

自作のストップウォッチクラスの解説については、前のNoteにて、解説していますので、クラスとは何かわからない方は下のNoteのリンクをクリックしてぜひご覧になってください〜😉↓

では、実践編の具体例として、

  • ストップウォッチ(親クラス)

  • タイマー(ストップウォッチの子クラス)

を例に考えていきましょう。では、コードを出します。長いですが、なんとか
ついっていってくださいね🤔💦

class StopWatch:#・・・親クラス(スーパークラス)
	'''
	ストップウォッチのように時間計測できるクラスです。
	'''
	def __init__(self,criterionTimeFunc=time.perf_counter):
		'''
		Parameters:

		criterionTimeFunc
			省略可。基準となるタイムのコールバック関数を入れます。省略すると、time.perf_counter関数が入ります。
		'''
		self._beginTime,self._endTime,self._splitTime,self._result=[None for i in range(4)]
		self._WholeSplitTime,self._WholeTime=[0 for i in range(2)]
		self._lapNum=1
		self._isStarted=False
		self._criterionTimeFunc=criterionTimeFunc
	def _getTotalTime(self):
		'''
		合計時間を返す内部変数です。このメソッドはプライベートメソッドです!!使わないでください!!
		'''
		return self._endTime-self._beginTime
	def _getSplitTime(self):
		'''
		スプリットした時間を計算する内部変数です。このメソッドはプライベートメソッドです!!使わないでください!!
		'''
		return self._endTime-self._splitTime
	def check(self):
		self._endTime=self._criterionTimeFunc()
		if self._WholeSplitTime ==0:
			self._result=self._WholeTime+self._getTotalTime()
		else:
			self._result=(self._lapNum,self._WholeSplitTime+self._getSplitTime(),self._WholeTime+self._getTotalTime())
		return self._result
	def start(self):
		'''
		ストップウォッチの計測を開始します。
		'''
		if not self._isStarted:
			self._beginTime=self._criterionTimeFunc()
			self._isStarted=True
	def stop(self):
		'''
		時間を計測を一旦終了します。

		Returns:
			<float>start関数を実行した時間を基準に、計測された時間を返します。

			<tuple>Split関数が使われたとき、「ラップ、スプリットされた時間、合計時間」をtuple型で返します。
		'''
		if self._isStarted:
			self._endTime=self._criterionTimeFunc()
			self._isStarted=False
			if self._splitTime is not None or self._WholeSplitTime!=0:
				self._WholeTime,self._WholeSplitTime=[self._WholeTime+self._getTotalTime(),self._WholeSplitTime+self._getSplitTime()]
				self._result=(self._lapNum,self._WholeSplitTime,self._WholeTime)
			else:
				self._result=self._WholeTime=self._WholeTime+self._getTotalTime()
			self._beginTime,self._endTime,self._splitTime=[None for i in range(3)]
			return self._result
		return None
	def reset(self):
		'''
		ストップウォッチをリセットする操作に相当します。すべての値をさらの状態に戻します。
		'''
		self._beginTime,self._endTime,self._splitTime,self._result=[None for i in range(4)]
		self._lapNum=1
		self._isStarted=False
	def split(self):
		'''
		ストップウォッチのsplitボタンを押し、ラップ数、スプリットした経過時間、合計時間を算出します。
		Returns:
		ラップ数、スプリットした経過時間、合計時間をTuple型でまとめて返します。
		'''
		if self._splitTime is None :
			self._splitTime,self._endTime=[self._criterionTimeFunc() for i in range(2)]
			self._result=(self._lapNum,self._WholeSplitTime+self._getTotalTime(),self._WholeTime+self._getTotalTime())
			self._wholeSplitTime=0
		else:
			if not self._isStarted:
				return None
			self._endTime=self._criterionTimeFunc()
			self._result=(self._lapNum,self._WholeSplitTime+self._getSplitTime(),self._WholeTime+self._getTotalTime())
			self._splitTime=self._endTime
		self._lapNum+=1
		return self._result
'''
	以下がTimerクラス。TimerStopWatchの継承クラスで、タイマーの役割を果たしています。
'''

class Timer(StopWatch):#子クラス
	def __init__(self,t_sec):
		self._tSec=t_sec
		self._restSec=0
		super().__init__()
	def check(self):
		self._restSec=self._tSec-super().check()
		return self._restSec if self._restSec>=0 else 0
	def stop(self):
		self._restSec=self._tSec-super().stop()
		return self._restSec if self._restSec>=0 else 0

上のコードのように

ストップウォッチのクラスを応用して、タイマーを作るぞっ🔥💪

と考えていたときに、

いや継承すると、ストップウォッチのクラスの分が使えてしまうし、ストップウォッチをメンバ変数に入れておくだけで良いのでは…🤔

と考えた時期にもありました。そして、あらためて、ストップウォッチのクラスはタイマークラスと…🤔↓

  • 関数の名前が共通している

  • タイマーがなる秒数、すなわちストップウォッチの「ストップ」関数で得られる経過時間が出せるのではないか?

と考えた時に、

ストップウォッチのクラスを継承する

ことで、

  • ストップウォッチの使い回し

  • コードを省略できる(何度も書かなくてもよい。)

メリットがあると考え、ストップウォッチのクラスから継承することにしました。すると、

class Timer(StopWatch):#子クラス
	def __init__(self,t_sec):
		self._tSec=t_sec
		self._restSec=0
		super().__init__()
	def check(self):
		self._restSec=self._tSec-super().check()
		return self._restSec if self._restSec>=0 else 0
	def stop(self):
		self._restSec=self._tSec-super().stop()
		return self._restSec if self._restSec>=0 else 0

上のコードのように、ストップウォッチで引き継いだ関数、

  • スタート

  • リセット

はそのまま使い回し、

  • チェック

  • ストップ

の関数の部分を少しアレンジ、親クラス、ストップウォッチクラス
のオーバーライドするだけで、

「タイマー」クラス

を作ることができました。結果、コードが少なく済みました☺️

仮に継承を入れないとなると、

class Timer():
	def __init__():
		#<省略>
	def start():
		#<省略>
	def stop():
		#<省略>
	def check():
		#<省略>

と使い回す関数があるのにもかかわらず、

同じ関数を使うハメになってしまい、かえってコードの量がふえてしまいます🥺💦

ちなみに、super()メソッドは、

スーパークラス=親クラス

を指しますよね?

ということは、ここでのsuper()メソッドの位置は

ストップウォッチクラスになります

から、ストップウォッチの機能に

「少しアレンジを加える」

だけで、タイマーの機能作れたというわけです。

継承されたクラスのインスタンスの中身はどんな感じになっているのか?

めっちゃ気になりませんか?(ぼくだけかな…🤔💦)

では、実際にインスタンスの中身をのぞいてみましょう!!そういうときは、dir関数を使うとよいでしょう!!

ではまず、親クラス(スーパークラス)ストップウォッチクラスから見ていきましょう!!

sw=StopWatch()
print(dir(sw))

では、上の長いコードの下に上のコードを入力し、実際にインスタンスの中身をのぞいてみましょう!!そういうときは、dir関数を使い、

メンバー変数&関数の名前

を引き出してしまいましょう!!

さきほどのタイマークラスとストップウォッチクラスのコードより下のコードをこれから伝えるコードに書きかえてみましょう!!

sw=StopWatch(12)
print('ストップウォッチクラス',dir(sw))#dir関数は値やメソッドの名前表示してくれる。

まずはスーパークラスのストップウォッチから見ていきます!!

ストップウォッチクラス ['_WholeSplitTime', '_WholeTime', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_beginTime', '_criterionTimeFunc', '_endTime', '_getSplitTime', '_getTotalTime', '_isStarted', '_lapNum', '_result', '_splitTime', 'check', 'reset', 'split', 'start', 'stop']

そもそも、dir関数は

値やメソッドの名前

を書き出してくれる関数です☺️では実行結果を見てみましょう!!

では同じようにして、下のコードを打ち込んで、Timerクラスのインスタンス変数の中身を表示していきます!!

print('Timerクラス:',dir(Timer(12)))
Timerクラス: ['_WholeSplitTime', '_WholeTime', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_beginTime', '_criterionTimeFunc', '_endTime', '_getSplitTime', '_getTotalTime', '_isStarted', '_lapNum', '_restSec', '_result', '_splitTime', '_tSec', 'check', 'reset', 'split', 'start', 'stop']

すると、あれっ!?みたことのあるようなないような…

メンバー変数、メソッドの名前にスーパークラスの分も含まれていることがわかりますね🤔

すなわち、継承を行うと、

スーパークラスの部分だけでなく、ストップウォッチクラスの分もひきつがれる

ことがわかります。継承を行うことで、親クラスからのメンバ変数やメソッドを引っ張ることができるので、

  • 「親クラス」をもとにアレンジしたい

  • 同じ役割を持つ関数がある

場合は、クラスを継承にするメリットはあると感じます。仮に、

  • 全く同じ場合、省略しても機能を引き継いでくれる

のも継承のメリットなのかなと感じています。ではあらためて、

ストップウォッチを継承したタイマークラスと親クラスであるストップウォッチクラスの関係図を見ていきましょう!!

継承すると、ヒロアカのOFAの継承者のごとく、技が使えるようにクラスの
継承でも、メソッドや変数が使えるようになっている(ヒロアカ知らない方はゴメンなさい💦)

まとめ

いかがでしたでしょうか。今回も実践編も交えて解説しました。
親クラスを継承するには

class Child(Parent): 
  <処理>

と書くとよいでしょう。
クラスの継承は

・親と子は似ている
・道場の師弟関係

のようにとらえると理解しやすいように感じます。なお、ヒロアカ知ってる方は、

「クラスの継承=OFAの継承」

ととらえると、スッキリするかもしれません!!以上でNoteを終わります!!お疲れ様でした〜!!

この記事が気に入ったらサポートをしてみませんか?