クラス継承は、既存のクラスを土台に新しいクラスを作り、共通の仕組みを保ちながら振る舞いを追加・変更できる強力な機能です。
同じようなコードを繰り返し書かずに再利用し、拡張で差分を表現できるのが最大の利点です。
本稿では、初心者の方にもわかりやすく、基本構文からオーバーライドの考え方、設計上の注意点まで順を追って解説します。
クラス継承とは(基底クラスとサブクラス)
継承は、あるクラス(基底クラス)の機能を別のクラス(サブクラス)が受け継いで使える仕組みです。
サブクラスは基底クラスの振る舞いをそのまま利用したり、必要に応じて上書きして振る舞いを変えたりできます。
継承の目的(再利用/拡張)
継承の主目的は2つあります。
1つはコードの再利用、もう1つは安全な拡張です。
共通部分は基底クラスへまとめ、差異はサブクラスで表現するという整理により、重複を抑え、変更の影響範囲も小さくできます。
特に、規約や契約(インタフェースに相当)を守ることで、呼び出し側のコード変更を最小限に保てます。
用語整理(親クラス/子クラス)
Pythonでは、基底クラス(親クラス、スーパークラス)とサブクラス(子クラス、派生クラス)という用語がよく使われます。
サブクラスは基底クラスを括弧内に指定して定義します。
例えば、class Dog(Animal): ...
のように書きます。
受け継がれるもの(メソッド/属性)
継承によって受け継がれるのはクラスに生えている属性とメソッドです。
インスタンス生成後に作られるインスタンス属性は、基底クラスの__init__
が正しく実行されてはじめて利用可能になります。
下表は代表的な項目の整理です。
種別 | 具体例 | 受け継がれるか | 補足 |
---|---|---|---|
クラス属性 | species = "Unknown" | 受け継がれる | サブクラスやインスタンスから参照可能 |
メソッド | def speak(self): ... | 受け継がれる | サブクラスでオーバーライド可能 |
インスタンス属性 | self.name | 条件付き | 親の__init__ が呼ばれて初期化される |
ダンダーメソッド | __str__ など | 受け継がれる | 同名で定義すれば振る舞いを差し替え可能 |
「インスタンス属性は自動では増えない」点がつまずきやすいポイントです。
サブクラスで__init__
を作る場合は、親の__init__
を呼ぶ必要があることを覚えておきましょう。
継承の書き方(基本構文)
class Sub(Base)の宣言
継承の宣言は、クラス名の後ろに括弧で基底クラスを指定します。
もっともシンプルな継承はサブクラス側がpass
のみの形です。
# 基本的な継承の例
class Animal:
species = "Unknown" # クラス属性(全インスタンスで共有)
def speak(self):
# 共通の振る舞い
return "..."
class Dog(Animal): # Animalを継承
pass # 何も追加しない場合でも、Animalの機能をそのまま使える
dog = Dog()
print(dog.speak()) # 親クラスのメソッドをそのまま使える
print(dog.species) # 親クラスのクラス属性も参照できる
...
Unknown
継承した瞬間に、サブクラスは親の公開されたメソッド・属性へアクセスできます。
サブクラスから親のメソッドを使う
サブクラスは、親から受け継いだメソッドをそのまま呼べます。
サブクラス固有のメソッド内で、親のメソッド結果を組み合わせることもできます。
class Animal:
def speak(self):
return "..."
class Cat(Animal):
def meow(self):
# 親クラスから受け継いだメソッドを普通に呼び出す
base = self.speak() # Animal.speak()
return base + " meow"
c = Cat()
print(c.meow())
... meow
サブクラスからはself
を通じて親の公開メンバにアクセスできます。
親の同名メソッドを上書きした後で元の処理を呼びたいときはsuper()
を使います(詳細は別記事で深掘りしますが、本稿でも後述の拡張例で軽く触れます)。
注意点(同名の属性/メソッドの影響)
サブクラスで同名の属性やメソッドを定義すると、親の同名メンバは隠蔽(シャドーイング)されます。
意図しない上書きはバグの原因になります。
class Base:
x = 1 # クラス属性
class Sub(Base):
x = 2 # Base.x をシャドーイング(上書き)
b = Base()
s = Sub()
print(Base.x, Sub.x, s.x) # それぞれの見え方を確認
# インスタンス属性でさらに上書くこともできる
s.x = 99
print(s.x, Sub.x) # インスタンス属性が最優先で参照される
1 2 2
99 2
意図せず同名を使うと、想定外の値が参照されることがあります。
名前は役割が衝突しないよう慎重に設計しましょう。
メソッドのオーバーライド(上書き)の基本
オーバーライドのルール
オーバーライドは、親と同じ名前のメソッドをサブクラスで再定義して振る舞いを差し替えることです。
呼び出し側は同じ名前で呼ぶだけで、実行時に最も派生レベルの高い実装が選ばれます。
class Animal:
def speak(self):
return "..."
class Dog(Animal):
def speak(self):
return "Woof!"
def make_speak(a: Animal):
# ポリモーフィズム: 実際の型ごとに適切な実装が呼ばれる
print(a.speak())
make_speak(Animal())
make_speak(Dog())
...
Woof!
呼び出し側は共通のインタフェース(メソッド名)を使い、実装差はサブクラス側に閉じ込めます。
引数と戻り値の互換性
Pythonは動的型付けですが、親メソッドと「互換な」引数・戻り値を保つのが実務的には重要です。
互換性を崩すと、共通の呼び出しコードが壊れます。
class Animal:
def speak(self, punct: str = "") -> str:
return "..." + punct
class Parrot(Animal):
# NG例: 親と互換でないシグネチャ(位置引数volumeが必須)
def speak(self, volume: int) -> str:
return "squawk" * volume
def chat(a: Animal):
# 親の契約: 文字を1つ渡せば動く想定
print(a.speak("!"))
chat(Animal())
# 互換でないオーバーライドのため、以下はTypeErrorになる
try:
chat(Parrot())
except TypeError as e:
print("TypeError:", e)
...!
TypeError: speak() missing 1 required positional argument: 'volume'
修正案では、親の引数と同様のデフォルトや意味を保ちます。
class Parrot(Animal):
# OK例: 親と互換のシグネチャ(デフォルト引数も提供)
def speak(self, punct: str = "") -> str:
return "squawk" + punct
chat(Parrot())
squawk!
「同じ名前で呼べば同じように使える」ことが継承の価値です。
型ヒントを併用すると、互換性の崩れをツールで検出しやすくなります。
__init__をオーバーライドする場合
サブクラスで__init__
を作るときは、親の__init__
で初期化される属性を忘れずに設定する必要があります。
通常はsuper().__init__(...)
を呼びます。
class User:
def __init__(self, name: str):
self.name = name # 親が責任を持つ初期化
class Admin(User):
def __init__(self, name: str, level: int):
super().__init__(name) # 親の初期化を必ず呼ぶ
self.level = level # 追加の初期化
a = Admin("Alice", 3)
print(a.name, a.level)
# 失敗例: 親の__init__を呼ばないとnameが存在しない
class BadAdmin(User):
def __init__(self, name: str, level: int):
self.level = level # 親の初期化をスキップしてしまった
b = BadAdmin("Bob", 1)
# ここでAttributeError
try:
print(b.name)
except AttributeError as e:
print("AttributeError:", e)
Alice 3
AttributeError: 'BadAdmin' object has no attribute 'name'
親の初期化を呼び忘れると、必要なインスタンス属性が欠落して実行時エラーになります。
迷ったらsuper().__init__
を呼ぶと覚えておくと安全です。
親の処理を残して拡張する考え方
「丸ごと差し替え」ではなく、親の処理を残しつつ前後や一部を拡張するのが現場では有効です。
その場合、super()
で親の実装を呼び出します。
class Logger:
def save(self, message: str):
# 実際はファイルやDB保存だが、例示として標準出力に
print(f"save: {message}")
class TimestampLogger(Logger):
def save(self, message: str):
# ここで親の処理に前処理(装飾)を加える
import datetime as dt
ts = dt.datetime(2024, 1, 1, 0, 0, 0) # 例示用に固定の時刻
decorated = f"[{ts:%Y-%m-%d %H:%M:%S}] {message}"
super().save(decorated) # 親の保存処理を呼ぶ
log = TimestampLogger()
log.save("system started")
save: [2024-01-01 00:00:00] system started
拡張は「親の責務」を尊重し、変更は最小限に留めるのがコツです。
継承の設計指針とベストプラクティス
使うべき場面(is-aで判断)
継承は「AはBの一種である」(is-a)関係が成り立つときに選びます。
例えば、DogはAnimalの一種なので妥当です。
一方、「UserがLogを持つ」のようにhas-a関係なら、継承ではなく合成(オブジェクトを属性として持つ)を優先します。
過度な継承を避ける
継承階層が深くなるほど把握が難しくなります。
2〜3段程度に抑え、横への共通化はヘルパ関数や合成で対応すると保守性が高まります。
汎用の共通実装をまとめたいだけなら、ユーティリティ関数や委譲を検討してください。
テストと保守のしやすさ
継承は振る舞いが拡散しやすく、テスト観点が増えます。
親の契約(メソッド名、引数、戻り値の意味)を文書化し、サブクラスでも満たすことで、テストの指針が明確になります。
型ヒントや静的解析の導入は、互換性崩壊の早期検知に役立ちます。
まとめ
継承は、共通の振る舞いを親に集約し、差分をサブクラスで表現する再利用と拡張の要です。
基本はclass Sub(Base)
で宣言し、同名の属性やメソッドの上書きに注意します。
オーバーライドでは引数と戻り値の互換性を意識し、__init__
を上書きする場合はsuper().__init__
を忘れないことが重要です。
さらに、「親の処理を尊重しつつ必要な拡張を加える」姿勢が保守性を高めます。
is-a関係で使い、複雑化は避けながら、テストしやすい契約を守ることで、初めての継承でも安心して活用できるようになります。