この記事ではオブジェクト指向の「継承」を解説します。
継承とは
継承とは「あるクラスの機能を別のクラスに引き継ぐこと」です。
クラスは、メンバ変数(プロパティ)とメンバ関数(メソッド)を持っています。
継承を使うことで、これらを別のクラスに引き継ぐことができます。
たとえば、以下のようなPythonのクラスを考えます。
このクラスはCarクラスで、車の情報を取得する、加速するなどの機能を持っています。
class Car:
def __init__(self, make, model, year, color):
self.make = make
self.model = model
self.year = year
self.color = color
self.speed = 0
def accelerate(self, amount):
self.speed += amount
def brake(self, amount):
if self.speed - amount < 0:
self.speed = 0
else:
self.speed -= amount
def get_speed(self):
return self.speed
def get_info(self):
return f"{self.color} {self.year} {self.make} {self.model}"
このCarクラスの機能を別のクラスに継承しましょう。
例として電気自動車のクラスを作り、Carクラスを継承します。
class ElectricCar(Car): # Carクラスを継承
def __init__(self, make, model, year, color, battery_size):
super().__init__(make, model, year, color)
self.battery_size = battery_size
self.charge_level = 100
def charge(self, amount):
if self.charge_level + amount > 100:
self.charge_level = 100
else:
self.charge_level += amount
def drive(self, miles):
range = self.battery_size * 3
if miles > range:
print(f"この車がガソリン満タンで走れる距離は{range}マイルまでです")
else:
self.charge_level -= miles / 3
self.accelerate(miles)
一行目のclass ElectricCar(Car)
でクラスを宣言していますが、クラス名のあとに(Car)
と記述しています。
このようにPythonでは「class クラス名(継承したいクラスの名前)
」と書くことで別のクラスを継承したクラスを定義できます。
継承では、クラスのプロパティとメソッドが引き継がれます。
そのため、Carクラスを継承したElectricCarクラスでは、何も書かなくてもCarクラスのプロパティやメソッドを使うことができます。
my_electric_car = ElectricCar("Tesla", "Model S", 2022, "red", 100)
print(my_electric_car.get_info()) # Carクラスのメソッドを使って車の情報を表示する
my_electric_car.accelerate(20) # Carクラスのメソッドを使って車を加速する
print(my_electric_car.get_speed()) # Carクラスのメソッドを使って車の速度を表示する
ElectricCarクラスではget_info
メソッドやget_accelerate
メソッド、get_speed
メソッドを定義していません。
しかし、Carクラスからそれらのメソッドを継承しているため、呼び出すことができます。
このように継承を使うと、クラスの機能を別のクラスに引き継ぐことができるのです。
スーパークラスとサブクラス
継承されるクラス(先の例ではCarクラス)をスーパークラスといいます。
一方で、継承する側のクラス(先の例ではElectricCarクラス)をサブクラスといいます。
スーパークラスを親クラス、サブクラスを子クラスと呼ぶこともあります。
親から子に継承するという関係性から、こちらの呼び方のほうがわかりやすいかもしれません。
そのため、これ以降は、親クラスと子クラスという呼び方を使います。
オーバーライド
クラスを継承した後、子クラスで親クラスの機能を上書きすることができます。
これをオーバーライドといいます。
オーバーライドを使えば、親クラスの機能を活かしながら、子クラスに特有の機能を実装できます。
親クラスの機能を効率的に再利用でき、コードの実装が楽になります。
以下のコードでは、親クラスとしてAnimalクラスを定義して、その子クラスとしてDogクラスとBirdクラスを定義しています。
class Animal:
def make_sound(self):
print("アニマル!")
class Dog(Animal):
def make_sound(self): # メソッドをオーバーライドする
print("わんわん")
class Cat(Animal):
def make_sound(self): # メソッドをオーバーライドする
print("にゃーにゃー")
子クラスでは親クラスのmake_sound
メソッドをオーバーライドして、それぞれの動物の鳴き声を発するように上書きしています。
各クラスのmake_sound
メソッドを実行すると以下のようになります。
animal = Animal()
dog = Dog()
cat = Cat()
animal.make_sound() # 出力:アニマル!
dog.make_sound() # 出力:わんわん
cat.make_sound() # 出力:にゃーにゃー
子クラスで親クラスのメソッドの内容を上書きしたことによって、子クラス独自のメソッドが実行されていることがわかります。
このようにオーバーライドを使うと親クラスのメソッドを子クラスで上書きできます。
継承を使う場面
継承は処理の流れをコントロールするためによく使われます。
ここでは、WEBアプリケーションのフォームを作る場面を考えてみましょう。
WEBアプリケーションには「ログイン」や「新規登録」など、ユーザー情報を使用するフォームが実装されています。
ログインと新規登録のどちらのフォームでも「フォームの初期化」「入力データのチェック」「データの送信」などの機能は共通して必要になります。
こういった共通機能をひとつのクラスにまとめ、ログインが必要なユーザーにはログインフォームを表示し、新規登録が必要なユーザーには新規登録フォームを表示できるようにしたいです。
このような共通する処理の流れがあるときに継承を使うと便利です。
実際に作ってみましょう。
処理の流れを親クラスに定義する
まずはフォームの骨格となるクラスを定義します。
これが処理の流れを定義する親クラスになります。
class Form:
def __init__(self):
pass
def process_form(self):
# 処理の流れを定義する
self._initialize_form()
if self._validate_form():
self._process_data()
self._submit_form()
def _initialize_form(self):
# 具体的な処理は実装しない
pass
def _validate_form(self):
# 具体的な処理は実装しない
pass
def _process_data(self):
# 具体的な処理は実装しない
pass
def _submit_form(self):
# 具体的な処理は実装しない
pass
ここでは親クラスとしてFormクラスを定義しました。
Formクラスが「ログインフォーム」や「新規登録フォーム」の骨格となります。
ただし、Formクラスには具体的な処理を実装しません。
_initialize_form
や_validate_form
といったメソッドは定義しますが、その中には処理を書きません。
一方で、process_form
というメソッドをパブリックな(外部からもアクセス可能な)メソッドとして定義します。
このメソッドを実行すると「フォームの初期化」や「データのバリデーション」「データの処理」「送信処理」などが順に実行されます。
このようにFormクラスには「処理の流れ」だけを定義します。
具体的なデータの処理を子クラスに実装する
次に、Formクラスをベースにして「ログインフォーム」と「新規登録フォーム」を実装します。
Formクラスを継承して、LoginFormクラスとRegistrationFormクラスを定義します。
class LoginForm(Form):
def _initialize_form(self):
# ログインフォームの初期化処理を実装する
print("フォームを初期化しています...")
def _validate_form(self):
# メールアドレスやパスワードの検証を実装する
print("フォームのデータを検証しています...")
return True
def _process_data(self):
# 入力データの整形など必要な処理を実装する
print("フォームのデータを処理しています...")
def _submit_form(self):
# フォームデータの送信処理を実装する
print("フォームのデータを送信しています...")
class RegistrationForm(Form):
def _initialize_form(self):
# 新規登録フォームの初期化処理を実装する
print("フォームを初期化しています...")
def _validate_form(self):
# メールアドレスやパスワードの検証を実装する
print("フォームのデータを検証しています...")
return True
def _process_data(self):
# 入力データの整形など必要な処理を実装する
print("フォームのデータを処理しています...")
def _submit_form(self):
# フォームデータの送信処理を実装する
print("フォームのデータを送信しています...")
LoginFormクラスとRegistrationFormクラスでは、Formクラスのメソッドをオーバーライドして、具体的な処理を実装します。
これらのクラスを使うときは、Formクラスに定義したprocess_form
メソッドを呼び出します。
すると、process_form
メソッドに定義した「処理の流れ」に従って子クラスのメソッドが呼び出されます。
これによって「処理の流れ」はFormクラスで統一し、具体的な処理はLoginFormクラスやRegistrationFormクラスに分けることができます。
このように「処理の流れをコントロールするクラス」と「具体的な処理を実行するクラス」を分離することでコードの保守性を高めることができます。
このような使い方を「Template Methodパターン」といいます。
継承のメリット
クラスの継承には以下の3つのメリットがあります。
- コードが読みやすくなる(可読性)
- コードの修正や追加が簡単になる(保守性)
- コードを使いまわしやすくなる(再利用性)
1. コードが読みやすくなる(可読性)
継承を使うことでコードが読みやすくなる場合があります。
共通する処理を親クラスにまとめることで、コードの重複を避けることができるのです。
これによってコードが短くなり読みやすくなります。
Formクラスの例では、フォームの「処理の流れ」が親クラスのprocess_form
メソッドにまとめられていました。
「処理の流れ」を知りたいときはFormクラスを、具体的な処理の内容を知りたいときはLoginFormクラスやRegistrationFormクラスを見ればいいです。
このように、クラスの役割が明確に分離されるためコードが読みやすくなります。
2. コードの修正や追加が簡単になる(保守性)
継承を上手く使うと、コードの修正や追加が簡単になります。
Formクラスの例では、処理の全体の流れをFormクラスのprocess_form
メソッドにまとめました。
このとき_process_data
メソッドを_validate_form
メソッドの前に呼び出すような修正が必要になったとしましょう。
その修正は、Formクラスのprocess_form
メソッドでメソッドの呼び出し順を変えるだけで済みます。
子クラスであるLoginFormクラスやRegistrationFormクラスの実装を変える必要はありません。
親クラスに「処理の流れ」が集約されているため、修正が簡単です。
このように、継承を上手く使えばコードの追加や修正が簡単になります。
3. コードを使いまわしやすくなる(再利用性)
継承を使うと、コードを使い回すことができます。
親クラスに書いた処理を子クラスに引き継ぐことができるので、共通処理を親クラスにまとめれば子クラスで新たに実装する手間が省けるのです。
Formクラスの例では、process_form
メソッドの処理を子クラス側で使いまわしています。
子クラスには処理の流れを書かず、親クラスのメソッドを呼び出すだけでいいので簡単です。
抽象化すると扱いやすくなる
これまで見てきたように、継承の役割は「処理を抽象化すること」でもあります。
抽象化とは「複数のものに共通する重要な部分だけを抜き出すこと」です。
抽象化を上手く使うと、情報は扱いやすくなります。
私たちは、抽象化を使うことでちょっと楽をして生きています。
たとえば、友達や家族に「コンビニで甘いもの買ってきて」と頼むことがありますよね。
ここでは「コンビニ」と「甘いもの」が抽象化された情報です。
抽象化によって「具体的なコンビニ」や「具体的な商品」について考えることを避け、楽をしているのです。
もし、抽象化ができないとすると「家の向かいにあるセブンイレブンで『とろもちきなこわらび 黒蜜入り』を買ってきて」と頼まなければなりません。
あなたは「なんとなく甘いものが食べたい」のであって「どこのコンビニがいいか」「具体的にどんな商品を求めているか」は重要な情報ではありません。
しかし、抽象化ができないと、具体的なことを考えて頼まなければならず面倒です。
このように抽象化は情報を扱いやすくするのに大切な役割を果たしています。
クラスの継承とは、まさに情報を抽象化することです。
「具体的な処理」を子クラスに実装する一方で、「抽象化された処理」を親クラスにまとめることでコードが扱いやすくなるというわけです。
まとめ
この記事では、オブジェクト指向におけるクラスの「継承」について解説しました。
- 継承とは「あるクラスの機能を別のクラスに引き継ぐこと」
- メンバ変数(プロパティ)やメンバ関数(メソッド)を引き継ぐことができる
- 引き継がれるクラスをスーパークラス(親クラス)、引き継ぐクラスをサブクラス(子クラス)という
- 親クラスのメソッドを子クラスで上書きすることをオーバーライドという
- 継承は処理の流れを親クラスに、具体的な処理を子クラスに分離するのに便利(Template Methodパターン)
- 継承を上手く使うとコードの再利用性や保守性、可読性を高めることができる
クラスの継承は理解が難しい概念ですが、何度もコードを読み書きしているうちに継承の勘所がわかってくるはずです。
根気強く継承と向き合って、理解できるようになりましょう。