Python入門:オブジェクト指向プログラミングの継承・ポリモーフィズム・カプセル化を理解する

Pythonはシンプルで表現力豊かなプログラミング言語として知られており、業務アプリケーションやWebサービス、機械学習、データ解析など幅広い分野で利用されています。

本記事では、その中心的な特徴である「オブジェクト指向プログラミング(OOP)」の基本概念――継承、ポリモーフィズム、カプセル化――を初級者向けにやさしく、かつ実践的に解説します。

記事の最後には学んだ内容を確認できる演習問題と解答例を用意していますので、ぜひチャレンジしてください。


継承(Inheritance)

継承は、既存のクラス(親クラス/スーパークラス)の特性(属性やメソッド)を受け継いで新しいクラス(子クラス/サブクラス)を定義できる機能です。

重複コードを減らし、共通処理をまとめられます。

# 親クラス
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        pass  # 子クラスでオーバーライド(上書き)する

# 子クラス
class Dog(Animal):
    def speak(self):
        print(f"{self.name}はワンと鳴きます。")

class Cat(Animal):
    def speak(self):
        print(f"{self.name}はニャーと鳴きます。")

# 利用例
dog = Dog("ポチ")
dog.speak()  # ポチはワンと鳴きます。
cat = Cat("タマ")
cat.speak()  # タマはニャーと鳴きます。

上記のように、Animalクラスで共通の初期化処理をまとめ、DogやCatで固有のspeakメソッドを実装しています。

super() とは

Python の super() は、親クラス(基底クラス)のメソッドや属性にアクセスするための組み込み関数です。

特に多重継承やメソッド解決順序(MRO: Method Resolution Order)を意識したクラス設計で威力を発揮します。以下、ポイントごとに解説します。

単一継承の場合

class Parent:
    def greet(self):
        print("Hello from Parent")

class Child(Parent):
    def greet(self):
        super().greet()   # Parent.greet() を呼び出す
        print("... and Hello from Child")

c = Child()
c.greet()
# 出力:
# Hello from Parent
# ... and Hello from Child

super().greet() は、Child の親クラスである Parent の greet メソッドを呼び出します。

Python 3 では引数なしの super() が使えます(Python 2 では super(Child, self).greet() と書く必要がありました)。


なぜ super() を使うのか?

  1. メソッド連鎖(チェーン)を自動化
    複数クラスで同じ名前のメソッドをオーバーライドしつつ、すべての実装を呼び出したい場合、親クラスを明示的に指定するよりも、super() を使うと継承ツリーをまたいだ呼び出しが自動化されます。
  2. 多重継承との相性
    ダイヤモンド継承(共通の基底をもつ複数経路の継承)などでも、MRO に沿って一度ずつだけ呼び出しが行われるため、安全にメソッドを連鎖できます。

メソッド解決順序(MRO)とは?

MRO(Method Resolution Order、メソッド解決順序)とは、多重継承を行うクラス階層の中で、あるメソッドや属性を呼び出す際に「どのクラスをどの順番で探すか」を定めたルールのことです。

Python は C3 線形化アルゴリズムに従ってクラスの MRO を決定します。super() やクラスの属性参照時にこの順序に従って「次に呼ぶべきクラス」を動的に判断します。

なぜ MRO が必要か?

単一継承ならクラスの継承チェーンは一直線なので問題ありませんが、多重継承になると下記のように「どちらの親クラスを先に見るべきか」が曖昧になります。

   A
 / \
B     C
 \ /
   D

MRO がなければ、D().some_method() が呼ぶべき some_method が B → A → C → A… なのか、C → A → B → A… なのか不明瞭になります。C3 線形化は以下の要件を満たすよう順序を決めます。

  1. 各クラスは自身より先に探索されない
  2. 親クラスリストに現れる順序を維持する
  3. ダイヤモンド継承でもメソッドは一度だけ呼ばれる

MRO の確認方法

Python ではクラスオブジェクトの __mro__ 属性か、クラスメソッドの mro() で確認できます。

class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass

# タプルとして取得
print(D.__mro__)
# [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

# リストとして取得
print(D.mro())
# [D, B, C, A, object]

この順番に沿って、まず D → B → C → A → object の順でメソッドや属性が探索されます。


C3 線形化のイメージ

C3 アルゴリズムは簡単に言うと、各クラスの「直近の親リスト」と「親自身の線形化リスト」をマージしながら不整合が起きないよう順序を決めます。

上の例だと、

  • B の線形化: [B] + merge([A], [A, object], [A, object]) → [B, C, A, object]
  • C の線形化: [C] + merge([A], [A, object], [A, object]) → [C, B, A, object]

これらを D の直近親リスト [B, C] と共にマージし、重複を避けつつ順序を尊重していくイメージです。


super() と MRO の関係

super() は「MRO 上で自分の次に来るクラス」を動的に見つけ、そのクラスのメソッドを呼び出します。

class A:
    def go(self): print("A")

class B(A):
    def go(self):
        print("B")
        super().go()

class C(A):
    def go(self):
        print("C")
        super().go()

class D(B, C):
    def go(self):
        print("D")
        super().go()

D().go()
# 出力:
# D
# B
# C
# A

ここでは D の MRO が [D, B, C, A, object] なので、super() は順に B → C → A の go() を呼んでいます。


複雑な階層での注意点

  • キーワード引数+*args, **kwargs を使っておかないと、あるクラスに引数が渡らずエラーになることがある。
  • 明示的に親クラスを呼ぶ(A.method(self))と MRO をバイパスしてしまい、多重継承の連鎖が切れるので要注意。
  • 自分で定義・調査する場合は Class.mro() を適宜チェックしましょう。

super() を使う際の注意点

  1. 必ず新スタイルクラス
    Python3 ではすべて新スタイルクラスですが、Python2 の場合は必ず class X(object): と継承してください。
  2. 直接親クラスを呼ぶ場合との違い
    明示的に Parent.method(self, …) と書くと MRO を無視してしまい、多重継承のチェーンが崩れる可能性があります。
  3. 引数の受け渡しミス
    コンストラクタなどで *args, **kwargs の受け渡しを忘れると、期待したメソッドが呼び出されないことがあります。

ポリモーフィズム(Polymorphism)

ポリモーフィズムは「多態性」とも呼ばれ、異なるクラスのオブジェクトを同じインターフェース(メソッド呼び出し)で扱える仕組みです。

共通の基底クラスやプロトコルを持つクラス群を統一的に操作できます。

animals = [Dog("ポチ"), Cat("タマ")]
for animal in animals:
    animal.speak()

この例では、DogもCatもAnimalを継承しており、どちらもspeakメソッドを持つため、ループ内で共通のコードで呼び出せます。これにより、拡張性や柔軟性が向上します。


カプセル化(Encapsulation)

カプセル化とは、オブジェクトが内部で持つデータ(属性)や実装の詳細を外部から直接アクセスできないように隠蔽し、公的なインターフェース(メソッド)を通してのみ操作させる手法です。

これにより、データの不正な変更を防ぎ、クラスの内部設計を柔軟に変更できます。

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.__balance = balance  # __を付けるとアクセス制限される

    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount
            print(f"{amount}円を入金しました。残高: {self.__balance}円")

    def withdraw(self, amount):
        if 0 < amount <= self.__balance:
            self.__balance -= amount
            print(f"{amount}円を出金しました。残高: {self.__balance}円")
        else:
            print("出金額が不正です。")

account = BankAccount("佐藤花子", 1000)
account.deposit(500)
account.withdraw(200)
# print(account.__balance)  # エラー: 属性に直接アクセスできない

上記では__balance属性に直接アクセスできず、メソッド経由でのみ残高を変更します。


まとめ

本記事ではPythonのOOPの三大要素である「継承」「ポリモーフィズム」「カプセル化」を初級レベルで解説しました。

いずれもコードの再利用性・可読性・保守性を高めるために欠かせない概念です。

次のステップとしては、ダックタイピングやメタクラス、非同期処理と組み合わせた応用例などに挑戦してみると理解が深まります。


演習問題

以下の設問に挑戦し、実際にPythonコードを書いて動作を確認してください。

問題1

Vehicleという親クラスを定義し、CarとBikeの子クラスを作成してください。

各クラスにはmoveメソッドを定義し、Carは「自動車が走る」、Bikeは「自転車が走る」と出力するようにします。

問題2

Shapeという抽象的な図形クラスを定義し、Rectangle(長方形)とCircle(円)の子クラスを作成してください。

それぞれ、面積を計算するareaメソッドを実装し、リストでまとめて多態的に面積を出力してください。

問題3

Studentクラスを作成し、名前(name)、点数(score)を属性とします。

点数はカプセル化のために外部から直接変更できないように隠し、get_scoreとset_scoreメソッドを通じて取得・変更できるように実装してください。


解答例

問題1 の解答例

class Vehicle:
    def move(self):
        pass

class Car(Vehicle):
    def move(self):
        print("自動車が走る")

class Bike(Vehicle):
    def move(self):
        print("自転車が走る")

vehicles = [Car(), Bike()]
for v in vehicles:
    v.move()

問題2 の解答例

import math

class Shape:
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    def area(self):
        return math.pi * self.radius ** 2

shapes = [Rectangle(3, 5), Circle(2)]
for s in shapes:
    print(f"面積: {s.area():.2f}")

問題3 の解答例

class Student:
    def __init__(self, name, score):
        self.name = name
        self.__score = score

    def get_score(self):
        return self.__score

    def set_score(self, new_score):
        if 0 <= new_score <= 100:
            self.__score = new_score
        else:
            print("無効な点数です。")

student = Student("田中一郎", 75)
print(student.get_score())  # 75
student.set_score(90)
print(student.get_score())  # 90
student.set_score(150)      # 無効な点数です。